Datalytics

Блог Алексея Макарова о веб-аналитике, анализе данных и не только.
Группа в Фейсбук
Канал в Телеграм

Углубленное изучение Pandas: структуры данных

Есть у меня определенное мнение, что понимание базовых структур данных в Pandas крайне важно, потому что это дает нам представление о фундаментальной основе тех операций, которые мы выполняем с данными. Pandas очень прост и предоставляет простой способ организации данных и манипулирования ими. Но часто бывает так, что для того, чтобы сделать что-то, выходящее за рамки мануалов из сети, мы должны сначала понять основные принципы, на которых построена технология. Структуры данных Series и DataFrame — это строительные блоки, из которых построена работа с данными в Pandas. И их понимание может помочь нам лучаше понять Pandas как продвинутый инструмент. Благодаря лучшему пониманию структур данных нам будет легче создавать эффективные пайплайны обработки данных, легче читать и поддерживать код, делать его более переиспользуемым. В конечном счете, понимая как Pandas работает на более глубоком уровне, позволит раскрыть потенциал Pandas на полную.

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


Введение

Pandas — одна из наиболее важных и широко используемых библиотек анализа данных в Python. Чтобы эффективно использовать Pandas, важно понимать две основные структуры данных, которые он предлагает: Series и DataFrame.

Series и DataFrames являются важными структурами данных, которые используются для хранения, обработки и анализа данных в Pandas. В этом уроке мы дадим всестороннее представление об этих структурах данных, включая их создание и индексация внутри этих структур данных. Также поделимся особенностями работы этих структур.

Этот пост будет посвящен следующим темам:

  • Объяснение разницы между Series и DataFrame;
  • Объяснение структуры данных Series в Pandas;
  • Обсуждение разницы между списком и Series;
  • Обсуждение того, как библиотека Pandas автоматически присваивает тип данных элементам в серии;
  • Объяснение структуры и свойств DataFrame, включая его столбцы, индекс и взаимосвязь между столбцами и строками как объектами Series.

II. Разница между Series и DataFrame

Представьте, что у вас есть последовательность точек данных, представляющих одну переменную, такую как рост, вес или возраст. Эта последовательность (мы её ещё можем назвать серией точек) точек данных может быть представлена с помощью объекта Series в Pandas. Series (можно перевести как «ряд») — это одномерная структура данных, которая содержит последовательность значений и похожа на список или массив.

Вот пример Series:


0    78
1    53
2    76
3    67
4    60
Name: age, dtype: int64

Теперь представьте, что у вас есть сетка точек данных, представляющих множество переменных, таких как рост, вес, возраст и город для нескольких человек. Эта сетка точек данных может быть представлена с помощью DataFrame в Pandas. DataFrame — это двумерная структура данных, которая содержит несколько столбцов данных, каждый из которых имеет разное имя переменной.

Вот пример DataFrame:


       height     weight  age
0  187.640523  83.831507   78
1  174.001572  51.522409   53
2  179.787380  52.295150   76
3  192.408932  74.693967   67
4  188.675580  53.268766   60

Разницу между Series и DataFrame можно сравнить с разницей между линией и квадратом. Линия является одномерной и может содержать только одну последовательность точек данных, в то время как квадрат является двумерным и может содержать несколько последовательностей точек. Таким же образом, Series содержит одну последовательность точек данных, в то время как DataFrame содержит несколько последовательностей точек данных в разных столбцах.

Итак, при работе с данными в Pandas важно понимать разницу между Series и DataFrame и выбирать соответствующую структуру данных на основе размерности ваших данных.


III. Структура данных Series

Series — это одномерный индексированный массив, способный содержать любой тип данных.

Создать Series в Pandas довольно просто. Чтобы создать Series, вы просто передаете список значений в виде списка в функцию `pd.Series()` и при желании указываете индекс. Вот пример:

import pandas as pd import numpy as np s = pd.Series([1, 3, 5, np.nan, 6, 8]) print(s)

0    1.0
1    3.0
2    5.0
3    NaN
4    6.0
5    8.0
dtype: float64

А вот пример создания Series с указанием индекса:

s = pd.Series([1, 3, 5, np.nan, 6, 8], index = [1,2,3,5,8,11]) print(s)

1     1.0
2     3.0
3     5.0
5     NaN
8     6.0
11    8.0
dtype: float64


Автоматическое определение типов

Когда вы передаете список в объект Series, Pandas автоматически определяет тип данных элементов в списке и присваивает серии соответствующий тип данных. Это означает, что вам не нужно вручную указывать тип данных, и Pandas позаботится об этом за вас.

Например, если вы передадите список целых чисел объекту Series, Pandas автоматически присвоит серии тип данных integer. Если вы передадите список строк, Pandas присвоит серии строковый тип данных. Это делает работу с данными в Pandas удобной и легкой, так как вам не нужно беспокоиться о ручном указании типа данных для каждой серии.

Вот пример, иллюстрирующий эту мысль:


# Создаем Series из списка целых чисел
integer_series = pd.Series([1, 2, 3, 4, 5])
print("Data type of integer series:", integer_series.dtype)

# Создаем Series из списка строк
string_series = pd.Series(["apple", "banana", "cherry", "date"])
print("Data type of string series:", string_series.dtype)


Data type of integer series: int64
Data type of string series: object

Как вы можете видеть, Pandas автоматически определил тип данных элементов в списке и присвоил Series соответствующий тип данных. В первом случае список целых чисел передается объекту Series, поэтому Pandas присваивает серии целочисленный тип данных `int64`. Во втором случае список строк передается объекту Series, поэтому Pandas присваивает Series тип данных `object`, поскольку строки хранятся в Pandas как объекты.


Автоматическое определение типов при смешанных типах

Если в списке есть хотя бы один элемент с типом данных `str`, то Pandas присвоит объектный тип данных всей серии, независимо от типов данных других элементов в списке.

Вот пример, иллюстрирующий это:


# Создаем Series, содержащий как целые числа, так и строки
mixed_series = pd.Series([1, 2, "apple", 4, "banana"])
print("Data type of mixed series:", mixed_series.dtype)


Data type of mixed series: object

Как вы можете видеть, несмотря на то, что список, переданный конструктору pd.Series(), содержит как целые числа, так и строки, результирующий Series имеет тип данных `object`, поскольку в списке есть по крайней мере один элемент с типом данных `str`. Это означает, что все элементы в Series будут рассматриваться как объекты, независимо от их фактического типа данных.


Индексирование в Series

Индексирование и построение срезов (slicing) в Series работают аналогично индексации в списке или массиве. Вы можете получить доступ к отдельным элементам серии, используя квадратные скобки со значением индекса. Вот пример:


print(s[0])


1.0


print(s[:3])


0    1.0
1    3.0
2    5.0
dtype: float64


Атрибут index

Вызов атрибута `index` в Series позволяет получить объект, который содержит индексы для каждого элемента в серии. Вы можете использовать атрибут index для извлечения индексов для каждого элемента в Series и выполнения над ними различных операций.

Вот несколько примеров того, как использовать атрибут index в Series:


# Создание Series со стандартными индексами
default_index_series = pd.Series([1, 2, 3, 4, 5])

# Вызов индексов
print(default_index_series.index)


RangeIndex(start=0, stop=5, step=1)


# Создание Series с пользовательскими индексами
custom_index_series = pd.Series([1, 2, 3, 4, 5], index=["a", "b", "c", "d", "e"])

# Вызов индексов
print(custom_index_series.index)


Index(['a', 'b', 'c', 'd', 'e'], dtype='object')


# Изменение индексов
custom_index_series.index = ["x", "y", "z", "w", "v"]
print(custom_index_series)


x    1
y    2
z    3
w    4
v    5
dtype: int64

Как вы можете видеть, вы можете получить атрибут `index` из Series, просто вызвав `series.index`. Кроме того, вы можете изменить атрибут `index`, присвоив ему новое значение, как показано в примере выше. Это может быть полезно, когда вы хотите изменить метки для элементов в Series.


Разница и сходство Series и списков

Series и обычные списки в Python похожи: они одномерные, хранят элементы, у элементов есть индексы. Но в отличие от обычного списка, индексы в Series могут быть изменяемыми и не обязательно начинаться с нуля или располагаться в возрастающем порядке. Кроме того, индексы в серии Pandas могут быть как целыми числами, так и строками.

Вот пример, иллюстрирующий это:


# Создаем серию с кастомным индексом
custom_index_series = pd.Series([1, 2, 3, 4, 5], index=["a", "b", "c", "d", "e"])
print(custom_index_series)

# Создаем серию с неинкрементальным индексом
non_incremental_index_series = pd.Series([1, 2, 3, 4, 5], index=[1, 5, 2, 6, 3])
print(non_incremental_index_series)


a    1
b    2
c    3
d    4
e    5
dtype: int64
1    1
5    2
2    3
6    4
3    5
dtype: int64

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


Вы также можете использовать вместе с Series различные методы Pandas, такие как `.head()`, `.tail()`, `.describe()` и т. д. Вот пример:


print(s.head())


0    1.0
1    3.0
2    5.0
3    NaN
4    6.0
dtype: float64


print(s.describe())


count    5.000000
mean     4.600000
std      2.701851
min      1.000000
25%      3.000000
50%      5.000000
75%      6.000000
max      8.000000
dtype: float64

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


IV. Структура данных DataFrame

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

Создание DataFrame в Pandas выполняется с помощью конструктора `pd.DataFrame()`. Вы можете создать DataFrame из списка, словаря или массива NumPy. Вот пример использования словаря для создания DataFrame:


import pandas as pd

data = {
    'Name': ['John', 'Jane', 'Jim', 'Joan'],
    'Age': [32, 28, 41, 37],
    'City': ['New York', 'London', 'Paris', 'Berlin']
}
df = pd.DataFrame(data)
print(df)


   Name  Age      City
0  John   32  New York
1  Jane   28    London
2   Jim   41     Paris
3  Joan   37    Berlin

А вот пример создания DataFrame из списка списков:


# Создаем список списков
data = [['John', 25, 70, 180],
        ['Jane', 30, 65, 170],
        ['Jim', 35, 72, 185]]

# Создаем датафрейм из списка списков с колонками 'Name', 'Age', 'Weight', 'Height'
df = pd.DataFrame(data, columns=['Name', 'Age', 'Weight', 'Height'])

Этот код создает список списков под названием data. Каждый список внутри data представляет собой строку в DataFrame и содержит данные об имени, возрасте, весе и росте человека.

Затем мы создаем DataFrame из списка списков, передавая данные конструктору DataFrame. Мы также предоставляем список имен столбцов с аргументом columns.

Результирующий DataFrame будет выглядеть следующим образом:


   Name  Age  Weight  Height
0  John   25      70    180
1  Jane   30      65    170
2   Jim   35      72    185

Индексирование и срезы в DataFrame работают аналогично тому, как и в Series. Вы можете получить доступ к отдельным элементам, строкам или столбцам, используя квадратные скобки со значениями индекса. Вот пример:


print(df['Name'])


0    John
1    Jane
2     Jim
Name: Name, dtype: object


print(df[0:2])


   Name  Age  Weight  Height
0  John   25      70     180
1  Jane   30      65     170


Колонки — это Series

Когда вы вызываете колонки DataFrame, вы фактически получаете доступ к отдельным объектам Series, по одному для каждой колонки. Индекс внутри каждой Series совпадает с индексом DataFrame, что означает, что он содержит индексы строк внутри DataFrame.

Вот пример, иллюстрирующий это:


import pandas as pd

# Создаем DataFrame
df = pd.DataFrame({"A": [1, 2, 3, 4, 5],
                   "B": [6, 7, 8, 9, 10]})

# Получаем колонки
col_a = df["A"]
col_b = df["B"]

# Получаем индексы Series (колонок)
print(col_a.index)
print(col_b.index)

# Сравниваем индексы между собой
print(col_a.index == df.index)
print(col_b.index == df.index)


RangeIndex(start=0, stop=5, step=1)
RangeIndex(start=0, stop=5, step=1)
[ True  True  True  True  True]
[ True  True  True  True  True]

Как вы можете видеть, индекс как `col_a`, так и `col_b` совпадает с индексом DataFrame `df`, и сравнение между ними возвращает массив истинных значений, указывающий на то, что они действительно равны. Это означает, что вы можете использовать индекс DataFrame для доступа и манипулирования значениями в отдельных колонках, а также DataFrame в целом.


Атрибут name у Series

Атрибут `name` у Series позволяет получить название, присвоенное Series. Название используется для идентификации Series и может быть использовано как справочные данные, когда мы работаем с одинарной Series. В случае Series, полученного из DataFrame через доступ к его столбцам, значение атрибута `name` соответствует имени столбца, из которого был получен ряд.


np.random.seed(0)

data = {'height': np.random.normal(170, 10, 100),
        'weight': np.random.normal(65, 10, 100),
        'age': np.random.randint(18, 80, 100)}

df = pd.DataFrame(data)

height_series = df['height']

print(height_series.name)


height

Этот код выведет «height». «height» — название столбца исходного DataFrame, из которого была получена Series.


Строки — тоже Series

Когда вы извлекаете строку из DataFrame, вы тоже получаете объект Series. Индекс Series в этом случае выступает в качестве метки для каждого элемента в Series, что позволяет получать нужные данные. А ещё индекс Series эквивалентен именам колонок в DataFrame.


import pandas as pd
import numpy as np

np.random.seed(0)

data = {'height': np.random.normal(170, 10, 100),
        'weight': np.random.normal(65, 10, 100),
        'age': np.random.randint(18, 80, 100)}

df = pd.DataFrame(data)

first_row = df.loc[0]

print(first_row.index)

print(first_row['height'])


Index(['height', 'weight', 'age'], dtype='object')
187.64052345967664

Этот код выведет: Index([’height’, ’weight’, ’age’], dtype=’object’). Сначала мы получили первую строку из DataFrame (df.loc[0]) в виде Series. А затем вывели индекс этого Series. Видно, что он совпадает с именами колонок в DataFrame.

Когда вы извлекаете строку из DataFrame, вы получаете Series, и индекс серии действует как метка для каждого элемента в Series, позволяя вам ссылаться на данные при необходимости (например, таким образом выведено отдельное значение из first_row — first_row[’height’]). В этом случае индекс ряда эквивалентен именам столбцов в DataFrame.


Транспонирование

Сходство между строками и колонками в DataFrame приводит нас к операции транспонирования.

Транспонирование DataFrame — это процесс замены строк и столбцов фрейма данных так, чтобы колонки становились индексом, старый индекс становился названием колонок, а значения строк — значениями колонок. Операция транспонирования может быть сделана с помощью атрибута `T`

На уровне соединения столбцов и строк DataFrame состоит из нескольких Series, где каждый столбец представляет Series, а индекс каждой Series представляет индексы строк. Транспонирование DataFrame означает, что каждая Series-колонка становится новой строкой в транспонированном DataFrame, а метки строк каждой Series становятся новым индексом для транспонированного DataFrame.

Например, рассмотрим следующий код:


np.random.seed(0)

data = {'height': np.random.normal(170, 10, 100),
        'weight': np.random.normal(65, 10, 100),
        'age': np.random.randint(18, 80, 100)}

df = pd.DataFrame(data)

transposed_df = df.T

print(transposed_df)


                0           1          2           3           4           5   \
height  187.640523  174.001572  179.78738  192.408932  188.675580  160.227221   
weight   83.831507   51.522409   52.29515   74.693967   53.268766   84.436212   
age      78.000000   53.000000   76.00000   67.000000   60.000000   27.000000   

                6           7           8           9   ...          90  \
height  179.500884  168.486428  168.967811  174.105985  ...  165.968231   
weight   60.863810   57.525452   84.229420   79.805148  ...   52.071431   
age      62.000000   31.000000   75.000000   24.000000  ...   27.000000   

                91          92          93          94          95  \
height  182.224451  172.082750  179.766390  173.563664  177.065732   
weight   67.670509   64.607172   53.319065   70.232767   63.284537   
age      78.000000   45.000000   65.000000   53.000000   37.000000   

                96          97          98          99  
height  170.105000  187.858705  171.269121  174.019894  
weight   72.717906   73.235042   86.632359   78.365279  
age      30.000000   36.000000   67.000000   28.000000  

[3 rows x 100 columns]

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


V. Заключение

Библиотека Pandas для Python предоставляет эффективные структуры данных для хранения данных и манипулирования ими. Структуры Series и DataFrame являются основными объектами в Pandas. Series — это одномерный индексированный массив, а DataFrame — это двумерная структура данных с индексированными строками и столбцами.

DataFrame создаются из объектов Series, и каждая колонка в DataFrame представляет собой Series со своей собственной меткой. Таким же образом, когда мы обращаемся к строке в DataFrame, мы получаем объект Series, и индекс внутри этого объекта совпадает с индексом всего DataFrame.

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

Кого читать по теме аналитики данных

«Мы видим больше и дальше, чем они, не потому, что взгляд у нас острее и сами мы выше, но потому, что они подняли нас вверх и воздвигли на свою гигантскую высоту»

Для меня аналитика — это про людей. Не про компании и не про инструменты. И вот почему: люди вдохновляют на новые свершения, люди распространяют новые знания и придумывают новые методики, люди переосмысливают опыт и выкристаллизовывают его в алмазно-ослепляющую эссенцию. Весь мой путь в аналитике — это путь переосмысления опыта других профессионалов.

Наплывают воспоминания о том как в 2012 я изучал веб-аналитику с помощью блога web-analytic.ru, который вёл Даниил Азовских. С тех пор много воды утекло: сейчас Даниил уже не ведёт свой блог, а ещё я уже давно перестал читать блог Авинаша Кошика, но я с теплотой вспоминаю эти радостные моменты жадного поглощения новых знаний. Помню как статьи Олега Якубенкова помогли мне сделать тестовое задание на должность аналитика в Ингейт; статьи в блоге ohmystats Лёши Куличевского познакомили с когортным анализом и уже через несколько недель я хвастался перед коллегами тем, что умею строить когорты в Excel (в 2014 это был серьёзный повод похвастаться).

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

Check it out:

Я уверен, что есть те, кто активно делится знаниями по теме аналитики данных в блогах, каналах, социальных сетях, но не попал в эту подборку. Пишите в комментариях — исправим это недоразумение!

Головоломка про рандомный сэмпл

Введение

Иногда чтение чужого кода бывает крайне интересным и увлекательным, будто читаешь отличный нон-фикшн. Чаще бывает, что читаешь и понимаешь, что автор вообще не думал о том, что кто-то будет читать его творение. Будто намеренно обфусцировал. Еще бы переменные называл a,b,c,d и тогда можно было бы даже не пробовать разбираться.

И вот читая один ноутбук, я наткнулся на конструкцию, которая по началу ввела в ступор:

df.loc[df.index.isin(df.sample(int(len(df)*0.1)).index), 'some_column'] = 1

Немного подумав, я сообразил что таким образом делает автор и это решение показалось достаточно интересным, хотя и не достаточно прозрачным. Задача этой строчки кода — задать значение 1 в колонке some_column для 10% случайных строк в датафрейм.

У многих может возникнуть вопрос зачем вообще может понадобится такая операция. В изначальном ноутбуке этот кусок кода использовался, чтобы создать тестовый набор данных, обладающий заданными характеристиками: 10% должны были быть единицами, еще 20% двойками и т. д.

В этой статье мы детально разберем как работает эта отдельно взятая строчка кода (да, целая статья ради строчки кода, так она меня вдохновила). На самом деле, в этой строчке заложен целый алгоритм. Мы поэтапно рассмотрим каждый этап работы этого алгоритма. Это может быть полезно для начинающих изучать Pandas (и Python в целом) в качестве примера того, что даже странные вещи при декомпозиции кажутся простыми и понятными.

Создание тестового датафрейма

Первое, что мы сделаем — это создадим тестовый датафрейм.

Сначала создаем массив numpy (numpy.array) с помощью функции np.random.randint. Эта функция позволяет создать массив numpy заданной размерности (size) и заполнить его случайными целыми числами из определенного диапазона. Нижняя граница диапазона задается в параметре low, а верхняя — в high. Создадим двумерный массив размерностью 100 на 5 (или 100 массивов по 5 элементов в каждом) со случайными числами от 0 до 9 включительно и запишем его в переменную random_array:

import numpy as np
import pandas as pd
random_array = np.random.randint(low=0,high=10,size=(100,5))
display(random_array)

Out:

Из получившегося numpy-массива random_array сделаем датафрейм df:

df = pd.DataFrame(random_array,columns=['a','b','c','d','e'])
display(df.head())

Out:

Декомпозируй это

А теперь приступим по кусочкам разбирать исходную строку кода. Немного изменим её и вместо имени колонки some_column зададим имя колонки, которая уже присутствует в нашем датафрейме df, например, колонку e:

df.loc[df.index.isin(df.sample(int(len(df)*0.1)).index), 'e'] = 1

Мы будем записывать каждый этап алгоритма в отдельную переменную, чтобы повысить читабельность кода.

1. Определение длины датафрейма

Начнем с конструкции len(df):

Тут всё просто — стандартная питоновская функция len при передаче в неё датафрейма возвращает его длину. Запишем результат в переменную count_of_rows. Ожидаемо, count_of_rows у нас будет равняться 100:

count_of_rows = len(df)
print(count_of_rows)

Out:

2. Определение размера случайной выборки

Посмотрим что происходит на следующем этапе:

Тут полученное количество строк умножается на 0.1 и затем приводится к целому числу. То есть мы получаем 1/10 от длины датафрейма df. Запишем результат в переменную sample_size. В нашем случае это будет число 10:

sample_size = int(count_of_rows*0.1)
print (sample_size)

Out:

3. Формирование случайной выборки

Дальше нас встречает функция sample:

Эта функция нужна для получения случайной выборки из датафрейма. Мы передаем в функцию sample значение sample_size, тем самым указывая, что нам нужна выборка заданного размера (10 строк) и записываем выборку в переменную sample_from_df:

sample_from_df = df.sample(sample_size)
display(sample_from_df)

Out:

Обратите внимание, что датафрейм с выборкой, которую мы получили имеет индексы исходного датафрейма df. Если вам не понятно что такое индексы, то всё должно стать понятнее если назвать их номерами строк. Таким образом, мы легко понимаем какие именно случайные строки были выбраны для случайной выборки.

4. Получение индекса из случайной выборки

Следующий шаг — получение индекса строк из образовавшейся выборки:

Полученные индексы запишем в переменную index_of_sample_from_df:

index_of_sample_from_df = sample_from_df.index
display(index_of_sample_from_df)

Out:

5. Создание маски для выборки

Дальше нас встречает конструкция с оператором //isin:

На самом деле, isin тут лишний, но как оптимизировать этот код, убрав из него эту конструкцию, я расскажу в конце статьи.

Итак, что же делает функция isin? Когда она применяется к индексу, то она возвращает массив булевых (True/False) значений, где True будет соответствовать тем строкам датафрейма df, индексы которых находятся в массиве index_of_sample_from_df, а False — всем остальным строкам. Запишем такой массив в переменную binary_mask:

binary_mask = df.index.isin(index_of_sample_from_df)
display(binary_mask)

Out:

6. Присвоение значения в соответствии с маской

И последний этап нашего алгоритма — присвоить значение 1 в колонке e строкам датафрейма df, которые попали в случайную 10ти-процентную выборку:

Такие строки как раз можно выделить из датафрейма df, передав битовую маску binary_mask в функцию loc.

Таким образом, функция loc позволяет нам выбрать строки в соответствии с порядковыми номерами элементов массива binary_mask, которые равняются True, а также задать колонку e, значение которой надо изменить. На словах это звучит сложно, но всё станет ясно, когда мы применим функцию и посмотрим на результат:

df.loc[binary_mask, 'e'] = 1
display(df.loc[binary_mask])

Out:

Обратите внимание на индекс этой выборки. Он совпадает с индексом случайной выборки index_of_sample_from_df. Так и задумывалось! :)

df.loc[binary_mask].index == sorted(sample_from_df.index)

Out:

Заключение

Таким образом, рассмотрев одну строчку кода мы рассмотрели небольшой алгоритм преобразования данных и углубились внутрь работы множества функций Pandas: sample, index, isin, loc.

Можно ли было проще?

В завершение рассмотрим как можно было бы сделать этот код чуть более легким. На самом деле, нет нужды в использовании конструкции df.index.isin. Хотя в некоторых случаях, от этой функции есть польза, например, при работе с мультииндексами, но об этом как-нибудь в другой раз.

Переменная index_of_sample_from_df уже содержит индексы строк, передав которые в loc вместо binary_mask мы получим выборку строк в соответствии с индексами:

df.loc[index_of_sample_from_df, 'e'] = 1
display(df.loc[index_of_sample_from_df])

Out:

Учитывая это, наш код можно было бы сократить:

df.loc[df.sample(int(len(df)*0.1)).index, 'e'] = 1

Или в более читабельном, но более длинном, виде:

count_of_rows = len(df)
sample_size = int(count_of_rows*0.1)
sample_from_df = df.sample(sample_size)
index_of_sample_from_df = sample_from_df.index
df.loc[index_of_sample_from_df, 'e'] = 1

Я уверен, что у этой задачи есть и другие решения. Если вам придёт в голову своё решение — не стесняйтесь писать в комментариях.

На этом всё. Спасибо, что читаете!

Успехов!

Работаем с API Google Drive с помощью Python

Решил написать достаточно подробную инструкцию о том как работать с API Google Drive v3 с помощью клиентской библиотеки Google API для Python. Статья будет полезна тем, кому приходится часто работать с документами в Google Drive: скачивать и загружать новые документы, удалять файлы, создавать папки.

Также я покажу пример того как можно с помощью API скачивать файлы Google Sheets в формате Excel, или наоборот: заливать в Google Drive файл Excel в виде документа Google Sheets.

Использование API Google Drive может быть полезным для автоматизации различной рутины, связанной с отчетностью. Например, я использую его для того, чтобы по расписанию загружать заранее подготовленные отчеты в папку Google Drive, к которой есть доступ у конечных потребителей отчетов.

Все примеры на Python 3.

Создание сервисного аккаунта и получение ключа

Прежде всего создаем сервисный аккаунт в консоли Google Cloud и для email сервисного аккаунта открываем доступ на редактирование необходимых папок. Не забудьте добавить в папку файлы, если их там нет, потому что файл нам понадобится, когда мы будем выполнять первый пример — скачивание файлов из Google Drive.

Я записал небольшой скринкаст, чтобы показать как получить ключ для сервисного аккаунта в формате JSON.

Установка клиентской библиотеки Google API и получение доступа к API

Сначала устанавливаем клиентскую библиотеку Google API для Python

pip install --upgrade google-api-python-client

Дальше импортируем нужные модули или отдельные функции из библиотек.

Ниже будет небольшое описание импортируемых модулей. Это для тех кто хочет понимать, что импортирует, но большинство просто может скопировать импорты и вставить в ноутбук :)

  • Модуль service_account из google.oauth2 понадобится нам для авторизации с помощью сервисного аккаунта.
  • Классы MediaIoBaseDownload и MediaFileUpload, как ясно из названий, пригодятся, чтобы скачать или загрузить файлы. Эти классы импортируются из googleapiclient.http
  • Функция build из googleapiclient.discovery позволяет создать ресурс для обращения к API, то есть это некая абстракция над REST API Drive, чтобы удобнее обращаться к методам API.
from google.oauth2 import service_account
from googleapiclient.http import MediaIoBaseDownload,MediaFileUpload
from googleapiclient.discovery import build
import pprint
import io

pp = pprint.PrettyPrinter(indent=4)

Указываем Scopes. Scopes — это перечень возможностей, которыми будет обладать сервис, созданный в скрипте. Ниже приведены Scopes, которые относятся к API Google Drive (из официальной документации):

Как видно, разные Scope предоставляют разный уровень доступа к данным. Нас интересует Scope «https://www.googleapis.com/auth/drive», который позволяет просматривать, редактировать, удалять или создавать файлы на Google Диске.

Также указываем в переменной SERVICE_ACCOUNT_FILE путь к файлу с ключами сервисного аккаунта.

SCOPES = ['https://www.googleapis.com/auth/drive']
SERVICE_ACCOUNT_FILE = '/home/makarov/Google Drive Test-fc4f3aea4d98.json'

Создаем Credentials (учетные данные), указав путь к сервисному аккаунту, а также заданные Scopes. А затем создаем сервис, который будет использовать 3ю версию REST API Google Drive, отправляя запросы из-под учетных данных credentials.

credentials = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_FILE, scopes=SCOPES)
service = build('drive', 'v3', credentials=credentials)

Получение списка файлов

Теперь можно получить список файлов и папок, к которым имеет доступ сервис. Для этого выполним запрос list, выдающий список файлов, со следующими параметрами:

  • pageSize — количество результатов выдачи. Можете смело ставить максимальное значение 1000. У меня стоит 10 результатов, чтобы показать как быть, когда нужно получить результаты по следующей страницы результатов
  • параметр files() в fields — параметр, указывающий, что нужно возвращать список файлов, где в скобках указан список полей для файлов, которые нужно показывать в результатах выдачи. Со всеми возможными полями можно познакомиться в документации (https://developers.google.com/drive/api/v3/reference/files) в разделе «Valid fields for files.list». У меня указаны поля для файлов: id (идентификатор файла в Drive), name (имя) и mimeType (тип файла). Чуть дальше мы рассмотрим пример запроса с большим количеством полей
  • nextPageToken в fields — это токен следующей страницы, если все результаты не помещаются в один ответ
results = service.files().list(pageSize=10,
                               fields="nextPageToken, files(id, name, mimeType)").execute()

Получили вот такие результаты:

pp.pprint(results)
print(len(results.get('files')))

10

Получив из результатов nextPageToken мы можем передать его в следущий запрос в параметре pageToken, чтобы получить результаты следующей страницы. Если в результатах будет nextPageToken, это значит, что есть ещё одна или несколько страниц с результатами

nextPageToken = results.get('nextPageToken')
results_for_next_page = service.files().list(pageSize=10,
                               fields="nextPageToken, files(id, name, mimeType)",
                               pageToken=nextPageToken).execute()
print (results_for_next_page.get('nextPageToken'))

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

results = service.files().list(pageSize=10,
                               fields="nextPageToken, files(id, name, mimeType)").execute()
nextPageToken = results.get('nextPageToken')
while nextPageToken:
        nextPage = service.files().list(pageSize=10,
                                        fields="nextPageToken, files(id, name, mimeType, parents)",
                                        pageToken=nextPageToken).execute()
        nextPageToken = nextPage.get('nextPageToken')
        results['files'] = results['files'] + nextPage['files']
print(len(results.get('files')))

24

Дальше давайте рассмотрим какие ещё поля можно использовать для списка возвращаемых файлов. Как я уже писал выше, со всеми полями можно ознакомиться по ссылке. Давайте рассмотрим самые полезные из них:

  • parents — ID папки, в которой расположен файл/подпапка
  • createdTime — дата создания файла/папки
  • permissions — перечень прав доступа к файлу
  • quotaBytesUsed — сколько места от квоты хранилища занимает файл (в байтах)
results = service.files().list(
        pageSize=10, fields="nextPageToken, files(id, name, mimeType, parents, createdTime, permissions, quotaBytesUsed)").execute()

Отобразим один файл из результатов с расширенным списком полей. Как видно permissions содержит информацию о двух юзерах, один из которых имеет role = owner, то есть владелец файла, а другой с role = writer, то есть имеет право записи.

pp.pprint(results.get('files')[0])

Очень удобная штука, позволяющая сократить количество результатов в запросе, чтобы получать только то, что действительно нужно — это возможность задать параметры поиска для файлов. Например, мы можем задать в какой папке искать файлы, зная её id:

results = service.files().list(
    pageSize=5, 
    fields="nextPageToken, files(id, name, mimeType, parents, createdTime)",
    q="'1mCCK9QGQxLDED8_pgq2dyvkmGRXhWEtJ' in parents").execute()
pp.pprint(results['files'])

С синтаксисом поисковых запросов можно ознакомиться в документации. Ещё один удобный способ поиска нужных файлов — по имени. Вот пример запроса, где мы ищем все файлы, содержащие в названии «data»:

results = service.files().list(
    pageSize=10, 
    fields="nextPageToken, files(id, name, mimeType, parents, createdTime)",
    q="name contains 'data'").execute()
pp.pprint(results['files'])

Условия поиска можно комбинировать. Возьмем условие поиска в папке и совместим с условием поиска по названию:

results = service.files().list(
    pageSize=10, 
    fields="nextPageToken, files(id, name, mimeType, parents, createdTime)",
    q="'1uuecd6ndiZlj3d9dSVeZeKyEmEkC7qyr' in parents and name contains 'data'").execute()
pp.pprint(results['files'])

Скачивание файлов из Google Drive

Теперь рассмотрим как скачивать файлы из Google Drive. Для этого нам понадобится создать запрос request для получения файла. После этого задаем интерфейс fh для записи в файл с помощью библиотеки io, указав в filename название файла (таким образом, можно сохранять файлы из Google Drive сразу с другим названием). Затем создаем экземпляр класса MediaIoBaseDownload, передав наш интерфейс для записи файла fh и запрос для скачивания файла request. Следующим шагом скачиваем файл по небольшим кусочкам (чанкам) с помощью метода next_chunk.

Если из предыдущего описания вам мало что понятно, не запаривайтесь, просто укажите свой file_id и filename, и всё у вас будет в порядке.

file_id = '1HKC4U1BMJTsonlYJhUKzM-ygrIVGzdBr'
request = service.files().get_media(fileId=file_id)
filename = '/home/makarov/File.csv'
fh = io.FileIO(filename, 'wb')
downloader = MediaIoBaseDownload(fh, request)
done = False
while done is False:
    status, done = downloader.next_chunk()
    print ("Download %d%%." % int(status.progress() * 100))

Файлы Google Sheets или Google Docs можно конвертировать в другие форматы, указав параметр mimeType в функции export_media (обратите внимание, что в предыдущем примере скачивания файла мы использоали другую функцию get_media). Например, файл Google Sheets можно конвертировать и скачать в виде файла Excel.

file_id = '10MM2f3V98wTu7GsoZSxzr9hkTGYvq_Jfb2HACvB9KjE'
request = service.files().export_media(fileId=file_id,
                                             mimeType='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
filename = '/home/makarov/Sheet.xlsx'
fh = io.FileIO(filename, 'wb')
downloader = MediaIoBaseDownload(fh, request)
done = False
while done is False:
    status, done = downloader.next_chunk()
    print ("Download %d%%." % int(status.progress() * 100))

Затем скачанный файл можно загнать в датафрейм. Это достаточно простой способ получить данные из Google Sheet в pandas-dataframe, но есть и другие способы, например, воспользоваться библиотекой gspread.

import pandas as pd
df = pd.read_excel('/home/makarov/Sheet.xlsx')
df.head(5)

Загрузка файлов и удаление в Google Drive

Рассмотрим простой пример загрузки файла в папку. Во-первых, нужно указать folder_id — id папки (его можно получить в адресной строке браузера, зайдя в папку, либо получив все файлы и папки методом list). Также нужно указать название name, с которым файл загрузится на Google Drive. Это название может быть отличным от исходного названия файла. Параметры folder_id и name передаем в словарь file_metadata, в котором задаются метаданные загружаемого файла. В переменной file_path указываем путь к файлу. Создаем объект media, в котором будет указание по какому пути находится загружаемый файл, а также указание, что мы будем использовать возобновляемую загрузку, что позволит нам загружать большие файлы. Google рекомендует использовать этот тип загрузки для файлов больше 5 мегабайт. Затем выполняем функцию create, которая позволит загрузить файл на Google Drive.

folder_id = '1mCCK9QGQxLDED8_pgq2dyvkmGRXhWEtJ'
name = 'Script_2.py'
file_path = '/home/makarov/Script.py'
file_metadata = {
                'name': name,
                'parents': [folder_id]
            }
media = MediaFileUpload(file_path, resumable=True)
r = service.files().create(body=file_metadata, media_body=media, fields='id').execute()
pp.pprint(r)

Как видно выше, при вызове функции create возвращается id созданного файла. Можно удалить файл, вызвав функцию delete. Но мы этого делать не будет так как файл понадобится в следующем примере

service.files().delete(fileId='18Wwvuye8dOjCZfJzGf45yQvB87Lazbzu').execute()

Сервисный аккаунт может удалить ли те файлы, которые были с помощью него созданы. Таким образом, даже если у сервисного аккаунта есть доступ на редактирование папки, то он не может удалить файлы, созданные другими пользователями. Понять что файл был создан помощью сервисного аккаунта можно задав поисковое условие с указанием email нашего сервисного аккаунта. Узнать email сервисного аккаунта можно вызвав атрибут signer_email у объекта credentials

print (credentials.signer_email)
results = service.files().list(
    pageSize=10, 
    fields="nextPageToken, files(id, name, mimeType, parents, createdTime)",
    q="'namby-pamby@tensile-verve-232214.iam.gserviceaccount.com' in owners").execute()
pp.pprint(results['files'][0:3])

Дальше — больше. С помощью API Google Drive мы можем загрузить файл с определенным mimeType, чтобы Drive понял к какому типу относится файл и предложил соответствующее приложение для его открытия.

folder_id = '1mCCK9QGQxLDED8_pgq2dyvkmGRXhWEtJ'
name = 'Sample data.csv'
file_path = '/home/makarov/sample_data_1.csv'
file_metadata = {
                'name': name,
                'mimeType': 'text/csv',
                'parents': [folder_id]
            }
media = MediaFileUpload(file_path, mimetype='text/csv', resumable=True)
r = service.files().create(body=file_metadata, media_body=media, fields='id').execute()
pp.pprint(r)

Но ещё более классная возможность — это загрузить файл одного типа с конвертацией в другой тип. Таким образом, мы можем залить csv файл из примера выше, указав для него тип Google Sheets. Это позволит сразу же конвертировать файл для открытия в Гугл Таблицах. Для этого надо в словаре file_metadata указать mimeType «application/vnd.google-apps.spreadsheet».

folder_id = '1mCCK9QGQxLDED8_pgq2dyvkmGRXhWEtJ'
name = 'Sheet from csv'
file_path = '/home/makarov/notebooks/sample_data_1.csv'
file_metadata = {
                'name': name,
                'mimeType': 'application/vnd.google-apps.spreadsheet',
                'parents': [folder_id]
            }
media = MediaFileUpload(file_path, mimetype='text/csv', resumable=True)
r = service.files().create(body=file_metadata, media_body=media, fields='id').execute()
pp.pprint(r)

Таким образом, загруженный нами CSV-файл будет доступен как Гугл Таблица:

Ещё одна часто необходимая функция — это создание папок. Тут всё просто, создание папки также делается с помощью метода create, надо только в file_metadata указать mimeType «application/vnd.google-apps.folder»

folder_id = '1uuecd6ndiZlj3d9dSVeZeKyEmEkC7qyr'
name = 'New Folder'
file_metadata = {
    'name': name,
    'mimeType': 'application/vnd.google-apps.folder',
    'parents': [folder_id]
}
r = service.files().create(body=file_metadata,
                                    fields='id').execute()
pp.pprint(r)

Заключение
Все содержимое этой статьи также представлено в виде ноутбука для Jupyter Notebook.

В этой статье мы рассмотрели лишь немногие возможности API Google Drive, но одни из самых необходимых:

  • Просмотр списка файлов
  • Скачивание документов из Google Drive (в том числе, скачивание с конвертацией, например, документов Google Sheets в формате Excel)
  • Загрузка документов в Google Drive (также как и в случае со скачиванием, с возможностью конвертации в нативные форматы Google Drive)
  • Удаление файлов
  • Создание папок

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

Успехов!

Большая подборка полезных ссылок про Pandas

Я достаточно долго веду группу в Facebook и канал в Telegram, посвященные анализу данных на Python и за это время накопилось множество полезных ссылок на материалы о библиотеке Pandas. Решил, что будет здорово, если все они будут в одном месте, а не разбросаны по каналу/группе. Поэтому завёл табличку в Notion, где все ссылки протегированы и разбиты по языку (ru, eng). Также в документе прикреплена ссылка, с помощью которой можно порекомендовать материал, будь то ваша авторская статья или просто понравившийся материал с просторов интернета. Материал по pandas появится в подборке и с высокой вероятностью может быть опубликован в канале и группе.

Встречайте: подборка полезных материалов про библиотеку Pandas

Лайк, шер, пускай больше людей приобщится к использованию Pandas!

Как в Pandas разбить одну колонку на несколько

Введение в задачу

Решил начать рассматривать нетривиальные кейсы в Pandas, с которыми иногда сталкиваюсь при работе с данными. Опять же, нетривиальные они только на мой взгляд, потому что какие-то вещи я решаю впервые и они заставляют немного подумать :) Возможно, такие небольшие кейсы помогут аналитикам, если они увидят в своих «затыках» что-то похожее. Также, я не претендую на абсолютную правильность или универсальность решения. Таким образом, у подобной задачи может быть несколько правильных решений. Я буду рад, если в комментариях вы будете предлагать свои решения. Касательно универсальности решения, тут я имею в виду, что решение может быть применимо к конкретному датасету, но при этом может не работать, если датасет будет иметь какие-то существенные видоизменения.

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

Итак, у меня есть вот такой dataframe:

Скачать данные для датафрейма в csv можно тут.

Выведем первое значение из колонки new_values, чтобы лучше понять что же нам надо сделать:

Как видно, значения разделены знаком переноса строки (\n), а также каждое значение представлено в виде ключ=значение (например, ключом выступает date_start, а значением 2018-12-04).

Задача состоит в том, чтобы привести датафрейм к вот такому виду:

Решение №1

Итак, первое решение будет простым и достаточно коротким.

Сначала, нам понадобится пандосовская функция str.split (https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.str.split.html). Она просто разбивает столбец на список на основании разделителя

df['new_values'].str.split('\n')

Вот как выглядит отдельное значение:

df['new_values'].str.split('\n')[0]

Но у функции str.split есть замечательный параметр expand=True, позволяющий сразу сделать разбиение на колонки и получить датафрейм

new_df = df['new_values'].str.split('\n',expand=True)
new_df

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

new_df.columns=['date_start','component','customer']
new_df

Затем нам нужно избавиться в колонках от названия ключа и знака «равно». Сделаем это простым циклом с функцией str.replace (https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.str.replace.html):

for column in new_df.columns:
    new_df[column] = new_df[column].str.replace(column+'=','')
new_df

Осталось только соединить два датафрейма (исходный df и new_df) с помощью функции pd.concat (https://pandas.pydata.org/pandas-docs/version/0.23.4/generated/pandas.concat.html). Обратите внимание на параметр axis=1, который позволяет соединить датафреймы по столбцам, а не по строкам

final_df = pd.concat([df,new_df],axis=1)
final_df

Ну и выкинем из получившегося датафрейма ненужный нам теперь столбец new_values с помощью drop (https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.drop.html):

final_df = final_df.drop('new_values',axis=1)
final_df

Итоговый код для решения задачи выглядит так:

df = pd.read_csv('sample_data_1.csv')
new_df = df['new_values'].str.split('\n',expand=True)
new_df.columns=['date_start','component','customer']
for column in new_df.columns:
    new_df[column] = new_df[column].str.replace(column+'=','')
final_df = pd.concat([df,new_df],axis=1).drop('new_values',axis=1)

Решение №2

Другой способ, который я покажу, является более универсальным.

Представим, что исходный датафрейм отличается от того, что я показывал выше.

Скачать данные для датафрейма в csv можно тут.

Этот датафрейм отличается тем, что в некоторых строках колонки new_values отсутствует параметр date_start. Из-за этого при попытке сделать str.split с параметром expand=True мы получим вот такой датафрейм:

df['new_values'].str.split('\n',expand=True)

В этом случае, мы не сможем просто пройтись циклом по колонкам и получить нужные значения, так как значения по ключу не всегда однозначно находятся в одной колонке, а могут быть разбросаны по нескольким. Такая же ситуация могла бы быть, если бы перечисление значений шло не в одном порядке, а по-разному. Например в одной строке date_start=2018-12-28\ncomponent=abc\ncustomer=59352, а в другой component=abc\ncustomer=22080\ndate_start=2018-12-18

Чтобы обойти эту проблему нам нужно преобразовать эти данные в такую структуру, которую было бы удобно запихнуть в датафрейм и pandas сам бы смог разделить данные по нужным колонкам, опираясь на структуру данных. Одной из таких структур может быть список словарей (list of dicts). В виде списка словарей данные должны выглядеть вот так:

Когда мы получим такую структуру, то потом сможем преобразовать её в датафрейм, а полученный датафрейм соединить с исходным (как мы уже это делали выше с помощью функции pd.concat).

Итак, первое что мы сделаем, это разобъем колонку с помощью уже знакомой функции str.split, но без параметра expand=True. Это позволит нам сделать отдельный series, содержащий списки:

s = df['new_values'].str.split('\n')
s

Затем каждый из списков нам нужно преобразовать к словарю. То есть совершить вот такое преобразование:

Чтобы лучше понять суть преобразования покажу на примере одного списка s[0], а затем сделаем функцию, которую применим к каждому элементу в series.

Сначала сделаем внутри списка s[0] вложенные списки, сделав split каждого из элемента списка по знаку «равно» через list comprehension (подробнее про list comprehension можно прочитать в статье, скажу только, что это очень удобно):

splited_items = [a.split('=') for a in s[0]]
splited_items

Затем полученный список splited_items надо переделать в словарь. Для этого сделаем несложное преобразование — создадим словарь, после чего пройдемся циклом по каждому вложенному списку и назначим нулевой элемент вложенного списка ключом словаря, а первый элемент — значением по ключу:

dictionary = {}
for item in splited_items:
    key = item[0]
    value = item[1]
    dictionary[key] = value
dictionary

Сделаем функцию, которая делает вышеописанное преобразование:

def convertToDict(x):
    splited_items = [a.split('=') for a in x]
    dictionary = {}
    for item in splited_items:
        key = item[0]
        value = item[1]
        dictionary[key] = value
    return dictionary

После этого можно применить функцию к серии s с помощью apply (https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.apply.html):

s = s.apply(lambda x: convertToDict(x))
s

Затем применим функцию pd.Series к каждому из словарей, таким образом преобразовав каждый словарь в series, а вместе последовательность series образует датафрейм:

new_df = s.apply(pd.Series)
new_df

Следующим шагом соединим датафрейм new_df с исходным df и уберем столбец с помощью функции drop:

final_df = pd.concat([df,new_df],axis=1).drop('new_values',axis=1)
final_df

Итоговый код для обработки данных выглядит вот так:

def convertToDict(x):
    splited_items = [a.split('=') for a in x]
    dictionary = {}
    for item in splited_items:
        key = item[0]
        value = item[1]
        dictionary[key] = value
    return dictionary

df = pd.read_csv('sample_data_2.csv')
s = df['new_values'].str.split('\n')
s = s.apply(lambda x: convertToDict(x))
new_df = s.apply(pd.Series)
final_df = pd.concat([df,new_df],axis=1).drop('new_values',axis=1)

И в качестве бонуса, можно сделать код более удобочитаемым с помощью method chaining:

df = pd.read_csv('sample_data_2.csv')
new_df = df['new_values'].str.split('\n').\
    apply(lambda x: convertToDict(x)).\
    apply(pd.Series)
final_df = pd.concat([df,new_df],axis=1).drop('new_values',axis=1)

Решение Романа Шапкова

UPD от 30.01.2019:
Читатель Роман решил эту задачу с помощью регулярных выражений

import pandas as pd
import re

df = pd.read_csv('sample_data_2.csv')

pattern_date = '(\d{4}-\d{2}-\d{2})'
pattern_ncomp = 'component=(\w*)'
pattern_ncust = 'customer=(\d*)'

def find_pattern(string, pattern):

    """
    аргументы: string - текстовая строка для поиска
    pattern - шаблон регулярного выражения

    функция осуществляет поиск шаблона "pattern" в строке "string" используя правила регулярных выражений(RegExp).
    Если шаблон найден - возвращает значение, иначе - возвращает None
    """
    if re.search(pattern, string):
        return re.search(pattern, string).group(1)

df['start_date'] = df['new_values'].apply(lambda x: find_pattern(x,pattern_date))
df['component'] = df['new_values'].apply(lambda x: find_pattern(x,pattern_ncomp))
df['customer'] = df['new_values'].apply(lambda x: find_pattern(x,pattern_ncust))

df

Заключение

На этом всё. Надеюсь, этот пример решения задачи кто-то найдет интересным и научится из него каким-то новым приёмам в своей работе. Это причина, по который я решил выкладывать такие примеры — не просто рассказывать про то, как работают функции в pandas или предлагать готовые решения, а показать последовательность действий и методологию решения задачи, чтобы читатели могли перенести части этого решения в свои проекты.

Если пост оказался полезным, то буду рад отзывам в комментариях, это будет для меня сигналом, что подобные вещи надо продолжать делать. А ещё лучше пишите как бы вы решили такую задачу, скидывайте примеры решений.

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

Как использовать Google BigQuery с помощью Python

Что такое Google BigQuery

Google BigQuery — это безсерверное масштабируемое хранилище данных. Использование безсерверного (облачного) решения — хорошая идея, если у вас нет серьезного бэкграунда в администрировании баз данных. Такой подход позволяет сосредоточиться только на анализе данных, и не думать об инфраструктуре хранения данных (шардировании, индексации, компрессии). BigQuery поддерживает стандартный диалект SQL, так что любой, кто когда-либо пользовался SQLными СУБД, с легкостью может начать им пользоваться.

Начало работы с Google BigQuery и создание ключа для сервисного аккаунта

Я не буду подробно объяснять о том как начать работу с Google Cloud Platform и завести первый проект, об этом хорошо написано в статье Алексея Селезнева в блоге Netpeak. Когда у нас уже есть проект в Cloud Platform с подключенным API BigQuery, следующим шагом нужно добавить учетные данные.

  1. Переходим в раздел «API и сервисы > Учетные данные»:
  1. Нажимаем «Создать учетные данные > Ключ сервисного аккаунта»
  1. Заполняем параметры: пишем название сервисного аккаунта; выбираем роль (как показано на скриншоте ниже, но роль может зависеть от уровня доступов, которые вы хотите предоставить сервисному аккаунту); выбираем тип ключа JSON; нажимаем «Создать»
  1. Переходим в раздел «IAM и администрирование > Сервисные аккаунты»
  1. В колонке «Действия» для созданного нами сервисного аккаунта выбираем «Создать ключ»
  1. Выбираем формат ключа «JSON» и нажимаем создать, после чего будет скачан JSON-файл, содержащий авторизационные данные для аккаунта

Полученный JSON с ключом нам понадобится в дальнейшем. Так что не теряем.

Использование pandas-gbq для импорта данных из Google BiqQuery

Первый способ, с помощью которого можно загружать данные из BigQuery в Pandas-датафрейм — библиотека pandas-gbq. Эта библиотека представляет собой обертку над API Google BigQuery, упрощающая работу с данными BigQuery через датафреймы.
Сначала нужно поставить библиотку pandas-gbq. Это можно сделать через pip или conda:

pip install pandas-gbq
conda install pandas-gbq -c conda-forge

Я решил рассмотреть основы работы с Google BigQuery с помощью Python на примере публичных датасетов. В качестве интересного примера возьмем датасет с данными о вопросах на сервисе Stackoverflow.

import pandas as pd
from google.oauth2 import service_account

# Прописываем адрес к файлу с данными по сервисному аккаунту и получаем credentials для доступа к данным
credentials = service_account.Credentials.from_service_account_file(
    'my-bq-project-225910-6e534ba48078.json')

# Формируем запрос и получаем количество вопросов с тегом "pandas", сгруппированные по дате создания
query = '''
SELECT DATE(creation_date) as date, COUNT(id) as questions
FROM
  [bigquery-public-data:stackoverflow.posts_questions]
WHERE tags LIKE '%pandas%'
GROUP BY
  date
'''

# Указываем идентификатор проекта
project_id = 'my-bq-project-225910'

# Выполняем запрос с помощью функции ((https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_gbq.html read_gbq)) в pandas, записывая результат в dataframe
df = pd.read_gbq(query, project_id=project_id, credentials=credentials)

display(df.head(5))

Важное примечание: По умолчанию функция read_gbq в pandas использует диалект legacy SQL. Для того, чтобы задать диалект standart SQL нужно воспользоваться параметром dialect:

df = pd.read_gbq(query, project_id=project_id, credentials=credentials, dialect='standard')

Дальше немного поиграем с обработкой данных. Выделим из даты месяц и год.

df['month'] =  df['date'].values.astype('datetime64[M]') # Создаем новый столбец с месяцем
df['year'] =  df['date'].values.astype('datetime64[Y]') # Создаем новый столбец с годом

# Отображаем один день с максимальным количеством вопросов
display(df.sort_values('questions',ascending=False).head(1))

Cгруппируем данные по годам и месяцам и запишем полученные данные в датафрейм stats

stats = df.groupby(['year','month'],as_index=False).agg({'questions':'sum'}) # Группируем данные по году и месяцу, используя в качестве агрегирующей функции сумму количества вопросов

display(stats.sort_values('questions',ascending=False).head(5))

Посчитаем суммарное количество вопросов в год, а также среднее количество запросов в месяц для каждого года, начиная с января 2013 и по август 2018 (последний полный месяц, который был в датасете на момент написания статьи). Запишем полученные данные в новый датафрейм year_stats

year_stats = stats[(stats.month >= '2013-01-01') & (stats.month < '2018-09-01')].groupby(['year'],as_index=False).agg({'questions':['mean','sum']})

display(year_stats)

Так как 2018 год в наших данных неполный, то мы можем посчитать оценочное количество вопросов, которое ожидается в 2018 году.

year_stats['estimate'] = year_stats[('questions','mean')]*12

display(year_stats)

На основе данных от StackOverflow, можно сказать, что популярность pandas из года в год растёт хорошими темпами :)

Запись данных из dataframe в Google BigQuery

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

В датафрейме year_stats получился multiindex из-за того, что мы применили две агрегирующие функции (mean и sum). Чтобы нормально записать такой датафрейм в BQ надо убрать multiindex. Для этого просто присвоим dataframe новые колонки.

year_stats.columns = ['year','mean_questions','sum_questions','estimate']

После этого применим к датафрейму year_stats функцию to_gbq. Параметр if_exists = ’fail’ означает, что при существовании таблицы с таким именем передача не выполнится. Также в значении этого параметра можно указать append и тогда к существующим данным в таблице будут добавлены новые. В параметре private_key указываем путь к ключу сервисного аккаунта.

year_stats.to_gbq('my_dataset.my_table', project_id=project_id, if_exists='fail', private_key='my-bq-project-225910-6e534ba48078.json')

После выполнения функции в BigQuery появятся наши данные:

Итак, мы рассмотрели импорт и экспорт данных в BiqQuery из Pandas’овского датафрейма с помощью pandas-gbq. Но pandas-gbq разрабатывается сообществом энтузиастов, в то время как существует официальная библиотека для работы с Google BigQuery с помощью Python. Основные сравнения pandas-gbq и официальной библиотеки можно посмотреть тут.

Использование официальной библиотеки для импорта данных из Google BiqQuery

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

from google.cloud import bigquery

client = bigquery.Client.from_service_account_json(
    '/home/makarov/notebooks/my-bq-project-225910-6e534ba48078.json')

sql = '''
    SELECT DATE(creation_date) as date, DATE_TRUNC(DATE(creation_date), MONTH) as month, DATE_TRUNC(DATE(creation_date), YEAR) as year, COUNT(id) as questions
FROM
  `bigquery-public-data.stackoverflow.posts_questions`
WHERE tags LIKE '%pandas%'
GROUP BY
  date, month, year
    '''

project_id = 'my-bq-project-225910'

df2 = client.query(sql, project=project_id).to_dataframe()

display(df2.head(5))

Как видно, по простоте синтаксиса, официальная библиотека мало чем отличается от использования pandas-gbq. При этом я заметил, что некоторые функции, (например, date_trunc), не работают через pandas-gbq. Так что я предпочитаю использовать официальное Python SDK для Google BigQuery.

По умолчанию в официальном SDK используется диалект Standard SQL, но можно использовать Legacy SQL. С примером использования Legacy SQL можно ознакомиться по ссылке.

Чтобы импортировать данные из датафрейма в BigQuery нужно установить pyarrow. Эта библиотека обеспечит унификацию данных в памяти, чтобы dataframe соответствовал структуре данных, нужных для загрузки в BigQuery.

# Создаем тестовый dataframe
df = pd.DataFrame(
    {
        'my_string': ['a', 'b', 'c'],
        'my_int64': [1, 2, 3],
        'my_float64': [4.0, 5.0, 6.0],
    }
)
dataset_ref = client.dataset('my_dataset_2') # Определяем датасет
dataset = bigquery.Dataset(dataset_ref)
dataset = client.create_dataset(dataset) # Создаем датасет

table_ref = dataset_ref.table('new_table') # Определяем таблицу (при этом не создавая её)

result = client.load_table_from_dataframe(df, table_ref).result() # Тут данные из датафрейма передаются в таблицу BQ, при этом таблица создается автоматически из определенной в предыдущей строке

Проверим, что наш датафрейм загрузился в BigQuery:

Прелесть использования нативного SDK вместо pandas_gbq в том, что можно управлять сущностями в BigQuery, например, создавать датасеты, редактировать таблицы (схемы, описания), создавать новые view и т. д. В общем, если pandas_gbq — это скорее про чтение и запись dataframe, то нативное SDK позволяет управлять всей внутренней кухней

Ниже привожу простой пример как можно изменить описание таблицы

table = client.get_table(table_ref) # Получаем данные о таблице

table.description = 'Моя таблица' # Задаем новый дескрипшн для таблицы

table = client.update_table(table, ['description'])  # Обновляем таблицу, передав новый дескрипшн через API

print(table.description)

Также с помощью нативного Python-SDK можно вывести все поля из схеме таблицы, отобразить количество строк в таблице

for schema_field in table.schema: # Для каждого поля в схеме
    print (schema_field) # Печатаем поле схемы

print(table.num_rows) # Отображаем количество строк в таблице

Если таблица уже создана, то в результате новой передачи датафрейма в существующую таблицу будут добавлены строки

result = client.load_table_from_dataframe(df, table_ref).result() # Передаем данные из dataframe в BQ-table
table = client.get_table(table_ref) # Заново получаем данные о таблице
print(table.num_rows) # Отображаем новое количество строк

Заключение

Вот так, с помощью несложных скриптов, можно передавать и получать данные из Google BigQuery, а также управлять различными сущностями (датасетами, таблицами) внутри BigQuery.

Успехов!

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

С чего аналитику начать изучение Python

Многие аналитики задумываются об изучении Python, но не представляют себе первых шагов.

В первую очередь, тем кто не знаком с Python я бы рекомендовал установить дистрибутив Anaconda. Это удобнее, чем устанавливать чистый Python, т. к. Anaconda содержит большинство пакетов, необходимых для анализа данных.

Следующий шаг — выбор среды разработки. Для анализа данных лучше всего подойдет Jupyter Notebook. Эта среда разработки устанавливается вместе с Anaconda. Вот простой туториал по работе с Jupyter Notebook.

Тем, кто вообще никогда не сталкивался с языками программирования (например, не писал на Паскале или Бейсике в школе), я бы посоветовал пройти любые курсы базового питона. Например, на Stepik или Codecademy.

Многие аналитики начинают учить Python, но быстро бросают. Чаще всего это происходит потому, что люди начинают изучение с синтаксиса и простых абстрактных примеров. Поначалу это может быть интересным, но потом надоедает. Лучше всего проходить основы языка (на курсах или с помощью учебника), но параллельно попробовать решать простые практические задачи, автоматизируя рутину и сразу же ощущая как Python улучшает вашу жизнь.

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

На мой взгляд, самая главная библиотека для аналитика — Pandas. Если вы хотите быстро очищать, трансформировать, агрегировать, объединять и вообще всячески манипулировать табличными данными, то Pandas будет в этом надежным помощником. Для аналитика эта библиотека покрывает 90% задач. Про Pandas есть хорошая статья в блоге khashtamov.com (и весь блог годный!). Также советую почитать более хардкорную статью ребят из ODS. Если вы решите выбрать образовательные курсы — это отлично, но не советую надеяться, что выбрав какую-то одну образовательную программу вы получите всеохватывающий спектр знаний, поэтому вашей надёжной подмогой станет постоянное изучение различных материалов: статей в блогах (пример), видео (пример), онлайн-учебников (пример). Не забывайте про документацию и вопросы на стаковерфлоу — почти как кофе и сигареты — это комбинация.

А ещё я подготовил большую подборку ссылок про Pandas.

Начните использовать Python с решения какой-то простой практической задачи, например, выгрузки данных через API Яндекс.Метрики и сохранения полученных данных в Excel. Узнать как начать работать с API Яндекс.Метрики можно из моей статьи.

Данные из Яндекс.Метрики в Python можно получить с помощью вот такого простого сниппета. Начните с получения токена для API Яндекс.Метрики и выполните этот код в Jupyter Notebook. Вы удивитесь как это просто!

Дальше можно усложнять скрипт, например, сделать несколько различных запросов и выгрузить данные на несколько вкладок в одном Excel-файле. Или выгрузить из Метрики данные с множеством dimensions и попробовать на их основе сделать в Pandas несколько таблиц с группировкой с помощью функции groupby, а также сводные таблицы с помощью функции pivot_table.

Успехов в автоматизированной борьбе с рутиной!

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

Группа в Facebook про анализ данных с помощью Python

Создал группу, посвященную анализу данных с помощью Python. Не столько про машинное обучение, сколько про подготовку/очистку/предобработку данных, использование Python для получения данных из API, парсинга веб-сайтов, автоматизации различной рутины. Группа предназначена для обмена опытом, взаимопомощи, поиска единомышленников для проектов. Приветствуются вопросы, обсуждения, ссылки на полезные материалы и инструменты. Постараюсь кидать максимум полезностей как для новичков, так и для проскилленных ребят.

https://www.facebook.com/groups/pydata/

Делаем сессии из лога событий с помощью Pandas

Предыстория

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

Структура данных лога представляет собой:

  • id — порядковый номер события в логе
  • user_id — уникальный идентификатор пользователя, совершившего событие (при решении реальной задачи анализа лога в качестве user_id может выступать IP-адрес пользователя или, например, уникальный идентификатор cookie-файла)
  • date_time — время совершения события
  • page — страница, на которую перешел пользователь (для решения задачи эта колонка не несет никакой пользы, я привожу её для наглядности)

Задача состоит в том, чтобы разбить последовательность событий (просмотров страниц) на вот такие блоки, которые будут сессиями:

Говоря «разбить», я не имею в виду разделить и сохранить в виде разных массивов данных или ещё что-то подобное. Тут речь идёт о том, чтобы каждому событию сопоставить номер сессии, в которую это событие входит.

Критерий сессии в моем случае — она живет полчаса после предыдущего совершенного события. Например, в строке 6 пользователь перешел на страницу /catalog в 8:21, а следующую страницу /index (строка 7) посмотрел в 9:22. Разница между просмотром страниц составляет 1 час 1 минуту, а значит эти просмотры относятся к разным сессиям этого пользователя.

Все это дело я буду делать на Питоне при помощи Pandas в Jupyter Notebook. Вот ссылка на ноутбук.

Алгоритм

Итак, у нас есть ’event_df’ — это датафрейм, в котором содержатся данные о событиях в привязке к пользователям:

1 События сгенерированные разными пользователями идут в хронологическом порядке. Для удобства отсортируем их по user_id, тогда события каждого пользователя будут идти последовательно:

event_df = event_df.sort_values('user_id')

2 В колонке ’diff’ для каждого события отдельного пользователя посчитаем разницу между временем посещения страницы и временем посещения предыдущей страницы. Если страница была первой для пользователя, то значение в колонке ’diff’ будет NaT, т. к. нет предыдущего значения

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

event_df['diff'] = event_df.groupby('user_id')['date_time'].diff(1)

Кое-что уже проклевывается. Мы нашли такие события, которые будут начальными точками для сессий:

3 Из основного датафрейма ’event_df’ создадим вспомогательный датафрейм ’session_start_df’. Этот датафрейм будет содержать события, которые будут считаться первыми событиями сессий. К таким событиям относятся все события, которые произошли спустя более чем 30 минут после предыдущего, либо события, которые были первыми для пользователя (NaT в колонке ’diff’).

Также создадим во вспомогательном датафрейме колонку ’session_id’, которая будет содержать в себе id первого события сессии. Она пригодится, чтобы корректно отобразить идентификатор сессии, когда будем объединять данные из основного и вспомогательного датафреймов.

sessions_start_df = event_df[(event_df['diff'].isnull()) | (event_df['diff'] > '1800 seconds')]
sessions_start_df['session_id'] = sessions_start_df['id']

Вспомогательный датафрейм ’session_start_df’ выглядит так:

4 С помощью функции merge_asof объединим между собой данные основного и вспомогательного датафреймов. Эта функция позволяет объединить данные двух датафреймов схожим образом с левым join’ом, но не по точному соответствию ключей, а по ближайшему. Примеры и подробности в документации.

Для корректной работы функции merge_asof оба датафрейма должны быть отсортированы по ключу, на основе которого будет происходить объединение. В нашем случае это колонка ’id’.

Обратите внимание, что из датафрейма ’session_start_df’ я выбираю только колонки ’id’, ’user_id’ и ’session_id’, так как остальные колонки особо не нужны.

event_df = event_df.sort_values('id')
sessions_start_df = sessions_start_df.sort_values('id')
event_df = pd.merge_asof(event_df,sessions_start_df[['id','user_id','session_id']],on='id',by='user_id')

В итоге получаем вот такой распрекрасный объединенный датафрейм, в котором в колонке ’session_id’ указан уникальный идентификатор сессии:

Дополнительные манипуляции

1 Найдем события, которые были первыми в сессиях. Это будет полезно, если мы захотим определить страницы входа.

Обнаружить эти события предельно просто: их идентификаторы будут равны идентификаторам сессии. Для этого создадим колонку ’is_first_event_in_session’, в которой сравним между собой значения колонок ’id’ и ’session_id’.

event_df['is_first_event_in_session'] = event_df['id'] == event_df['session_id']

2 Вычислим время, проведенное на странице, руководствуясь временем посещения следующей страницы

Для этого сначала считаем разницу между предыдущей и следующей страницей внутри сессии. Мы уже делали такое вычисление, когда считали разницу между временем посещения страниц пользователем. Только тогда мы группировали по ’user_id’, а теперь будем по ’session_id’.

event_df['time_on_page'] = event_df.groupby(['session_id'])['date_time'].diff(1)

Но diff со смещением в 1 строку считает разницу между посещением последующей страницы относительно предыдущей, поэтому время пребывания на предыдущей странице будет записано в строку следующего события:

Нам нужно сдвинуть значение столбца ’time_on_page’ на одну строку вверх внутри отдельно взятой сессии. Для этого нам пригодится функция shift.

event_df['time_on_page'] = event_df.groupby(['session_id'])['time_on_page'].shift(-1)

Получили то, что нужно:

Значения в столбце ’time_on_page’ имеют специфический тип datetime64, который не всегда удобен для арифметических операций, поэтому преобразуем ’time_on_page’ в секунды.

event_df['time_on_page'] = event_df['time_on_page'] / np.timedelta64(1, 's')

Вуаля:

3 На основе полученных данных очень просто посчитать различные агрегаты

event_df['user_id'].nunique() # Количество пользователей
event_df['session_id'].nunique() # Количество сессий
event_df['id'].count() # Количество просмотров страниц (событий)
event_df['time_on_page'].mean() # Среднее время просмотра страниц

Заключение

Таким образом, используя несколько не самых очевидных функций в Pandas (например, merge_asof мне довелось применять впервые), можно формировать сессии на основе лога событий. Логом событий могут выступать логи сервера, какой-нибудь клик-стрим в SaaS-сервисах, сырые данные систем веб-аналитики.

Удачи и новых аналитических достижений!

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

Ранее Ctrl + ↓