Что нового в pandas 1.0?
В конце января 2020 вышло большое обновление библиотеки pandas – 1.0. Представляем вам обзор изменений и дополнений, которые по нашему мнению являются интересными и заслуживают внимания.
- pd.NA
- Типы pandas для работы со строками и boolean-значениями
- Инструмент конвертирования типов
- Конвертор в markdown
- Еще изменения и дополнения…
Мажорный релиз 1.0 принес с собой улучшения существующих инструментов, новые дополнения (они пока имеют статус экспериментальных), ряд изменений в API, которые ломают обратную совместимость, багфиксы и изменения, увеличивающие производительность библиотеки.
pd.NA
Первое, с чего хотелось бы начать – это pd.NA – “значение”, которое pandas будет использовать для обозначения отсутствующих данных.
В предыдущих версиях pandas для обозначения отсутствующих данных использовались следующие значения: NaN, NaT и None. NaN – это отсутствующее значение в столбце с числовым типом данных, оно является аббревиатурой от Not a Number (пришло из numpy: np.NaN). NaT – это отсутствующее значение для данных типа DateTime, аббревиатура от Not a Time (является частью библиотеки pandas). None используется если тип данных object, такой тип имеют, например, элементы типа str (пришло из Python).
Рассмотрим работу с отсутствующими данными на примерах:
> d = <"A":[None, "test2", "test3"], "B": [1.01, np.nan, 3.45], "C": [date(2019, 1, 29), datetime.now(), None], "D":[1, 2, None]>> df = pd.DataFrame(d)
Если создать набор данных с целыми числами, не указывая явно тип, то будет использовано значение по умолчанию:
> pd.Series([4, 5, None]) 0 4.0 1 5.0 2 NaN dtype: float64
Если тип указать, то в pandas 1.0 будет использован pd.NA:
>pd.Series([4, 5, None], dtype="Int64") 0 4 1 5 2 dtype: Int64
Либо можно задать pd.NA напрямую:
> pd.Series([4, 5, pd.NA], dtype="Int64") 0 4 1 5 2 dtype: Int64
Для строковых и boolean значений это работает аналогично:
> pd.Series([None, "test2", "test3"]) 0 None 1 test2 2 test3 dtype: object > pd.Series([None, "test2", "test3"], dtype='string') 0 1 test2 2 test3 dtype: string > pd.Series([True, False, None]) 0 True 1 False 2 None dtype: object > pd.Series([True, False, None], dtype='boolean') 0 True 1 False 2 dtype: boolean
Типы pandas для работы со строками и boolean-значениями
Появился тип StringDtype для работы со строковыми данными (до этого строки хранились в object-dtype NumPy массивах). При создании структуры pandas необходимо указать тип StringDtype либо string:
> pd.Series([None, "test2", "test3"], dtype=pd.StringDtype()) 0 1 test2 2 test3 dtype: string > pd.Series([None, "test2", "test3"], dtype='string') 0 1 test2 2 test3 dtype: string
Также добавлен тип BooleanDtype для хранения boolean значений. В предыдущих версиях столбец данных типа bool не мог содержать отсутствующие значения, тип BooleanDtype поддерживает эту возможность:
> pd.Series([False, True, None], dtype=pd.BooleanDtype()) 0 False 1 True 2 dtype: boolean > pd.Series([False, True, None], dtype='boolean') 0 False 1 True 2 dtype: boolean
Инструмент конвертирования типов
Метод convert_dtypes поддерживает работу с новыми типами:
> d = <"A":[None, "test2", "test3"], "B": [1, np.nan, 3], "C": [True, False, None], "D":[1, 2, None]>> df = pd.DataFrame(d) > df_conv = df.convert_dtypes() > df
> df_conv
> df.dtypes A object B float64 C object D float64 dtype: object > df_conv.dtypes A string B Int64 C boolean D Int64 dtype: object
Конвертор в markdown
В pandas 1.0 добавлен метод to_markdown() для конвертирования структур pandas в markdown таблицы:
> d = <"A":[None, "test2", "test3"], "B": [1, np.nan, 3], "C": [True, False, None], "D":[1, 2, None]>> df = pd.DataFrame(d) > print(df.to_markdown()) | | A | B | C | D | |---:|:------|----:|:------|----:| | 0 | | 1 | True | 1 | | 1 | test2 | nan | False | 2 | | 2 | test3 | 3 | | nan | > s = pd.Series([None, "test2", "test3"], dtype='string') > print(s.to_markdown()) | | 0 | |---:|:------| | 0 | | | 1 | test2 | | 2 | test3 |
Еще изменения и дополнения…
- Ускорение работы функций rolling.apply и expanding.apply;
- Возможность игнорирования индекса при сортировке DataFrame:
> df = pd.DataFrame() > df
> df.sort_values("A")
> df.sort_values("A", ignore_index=True)
- Более информативный info()
> d = <"A":[None, "test2", "test3"], "B": [1, np.nan, 3], "C": [True, False, None], "D":[1, 2, None]>> df = pd.DataFrame(d).convert_dtypes() > df.info() RangeIndex: 3 entries, 0 to 2 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 A 2 non-null string 1 B 2 non-null Int64 2 C 2 non-null boolean 3 D 2 non-null Int64 dtypes: Int64(2), boolean(1), string(1) memory usage: 212.0 bytes
Более подробно про остальные изменения и дополнения можете прочитать на официальной странице сайта pandas.
P.S.
Вводные уроки по “Линейной алгебре на Python” вы можете найти соответствующей странице нашего сайта . Все уроки по этой теме собраны в книге “Линейная алгебра на Python”.
Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas. Для начала вы можете познакомиться с вводными уроками. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.
Getting Pandas NaT to propagate like NaN
I’m trying to take the min and max of a couple Pandas Series objects containing datetime64 data in the face of NaT. np.minimum and np.maximum work the way I want if the dtype is float64. That is, once any element in the comparison is NaN, NaN will be the result of that comparison. For example:
>>> s1 0 0.0 1 1.8 2 3.6 3 5.4 dtype: float64 >>> s2 0 10.0 1 17.0 2 NaN 3 14.0 dtype: float64 >>> np.maximum(s1, s2) 0 10.0 1 17.0 2 NaN 3 14.0 dtype: float64 >>> np.minimum(s1, s2) 0 0.0 1 1.8 2 NaN 3 5.4 dtype: float64
This doesn’t work if s1 and s2 are datetime64 objects:
>>> s1 0 2199-12-31 1 2199-12-31 2 2199-12-31 3 2199-12-31 dtype: datetime64[ns] >>> s2 0 NaT 1 2018-10-30 2 NaT 3 NaT dtype: datetime64[ns] >>> np.maximum(s1, s2) 0 2199-12-31 1 2199-12-31 2 2199-12-31 3 2199-12-31 dtype: datetime64[ns] >>> np.minimum(s1, s2) 0 2199-12-31 1 2018-10-30 2 2199-12-31 3 2199-12-31 dtype: datetime64[ns]
I expected indexes 0, 2 and 3 to turn up as NaT whether computing the min or max. (I realize numpy’s functions might not have been the best choice, but I was not successful finding suitable Pandas analogs.) After doing a bit of reading, I came to realize NaT is only approximately NaN, the latter having a proper floating point representation. Further reading suggested no simple way to have NaT «pollute» these comparisons. What’s the correct way to get NaT to propagate in min/max comparisons the way NaN does in a floating point context? Maybe there are Pandas equivalents to numpy.
Делаем сессии из лога событий с помощью 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, там публикуются интересные статьи про анализ данных и не только.
Как правильно проверить на Null\NaT поле фраймворка pandas и записать его в базу postgres?
Последняя колонка содержит «NaT» , в базе данных данное поле update_dt типа timestamp(0).
psycopg2.errors.InvalidDatetimeFormat: ОШИБКА: неверный синтаксис для типа timestamp: «NaT»
Если сделать так df = df.where(pd.notnull(df), ‘Null’).
psycopg2.errors.InvalidDatetimeFormat: ОШИБКА: неверный синтаксис для типа timestamp: «Null»
Как проверить на Null и записать данные timestamp не используя case (if) sql.
- Вопрос задан 21 дек. 2022
- 523 просмотра
2 комментария
Простой 2 комментария
mayton2019 @mayton2019
Две просьбы.
1) Придумай названия колонкам. Чтоб не мучать форум такими формами как «последняя колонка или пред-последняя»
2) Оформи это в виде таблицы — тогда ясность появляется и все можено порешать.
Модератор @TosterModerator
Фрагменты кода надо размещать в виде текста и оборачивать тэгом code для корректного отображения. Удобно делать кнопкой >
Это обязательно, см.п.3.8 Регламента.
Сюда же относится traceback, ввод и вывод в консоли и другая структурированная текстовая инфа.
Если таблицу обернуть тэгом, тоже будет выглядеть лучше.
Решения вопроса 1
Совершенствуюсь каждый день
В pandas можно использовать функцию isnull для проверки поля на наличие значения NaN (Not a Number) или NaT (Not a Time). Например, чтобы проверить поле update_dt на наличие значения NaT, можно использовать следующий код:
df[‘update_dt’].isnull()
Этот код вернет булевый сериес, где True указывает на то, что в соответствующей ячейке поля update_dt стоит значение NaT, а False — значение присутствует.
Чтобы записать эти данные в базу данных, можно использовать конструкцию INSERT INTO . SELECT . FROM с вложенным запросом. Вот пример такого запроса, который записывает в таблицу table_name все записи из df, где поле update_dt не равно NaT:
INSERT INTO table_name (column1, column2, . update_dt) SELECT column1, column2, . update_dt FROM df WHERE df['update_dt'].isnull() = False