Следующая тема: ИАД. Работа с несколькими источниками данных
Вернуться в раздел: Исследовательский анализ данных
Вернуться в оглавление: Я.Практикум
1. Введение
2. Срезы данных и поиск авиабилетов
3. Срезы данных методом query()
6. "Слишком долгая" заправка - это сколько?
8. Графики
9. Группировка с pivot_table()
11. Диаграмма размаха в Python
13. Заключение
14. Проверочные задания. Изучение cрезов
Введение
Вы получили общее представление о данных — самое время исследовать их более тщательно.
Чему вы научитесь
- Получать срезы данных вручную и методом query().
- Округлять время и переводить его в другие часовые пояса.
- Строить графики методом plot().
- Составлять правильные баг-репорты.
Вам предстоит:
- Изучить АЗС со сверхдолгими заправками;
- Определить границу, после которой можно считать заправку «слишком долгой»;
- Узнать, чем ночные заезды отличаются от дневных;
- Найти аномально быстрые заправки.
Сколько времени это займёт
10 уроков от 5 до 30 минут.
Постановка задачи
Погрузитесь в детали: найдите аномально быстрые и сверхдолгие заправки. Узнайте, для каких АЗС они характерны.
Срезы данных и поиск авиабилетов
Пора перейти от общего представления о данных к деталям. Откуда взялись короткие заезды на АЗС? А очень долгие? Это характерно для одной АЗС или для всех? Отличаются ли чем-то АЗС со сверхдолгими заправками от других?
Чтобы ответить на эти вопросы, нужны не все данные, а лишь их часть — срез данных. Рассмотрим работу срезов на примере покупки авиабилетов.
Закройте глаза на секунду, помечтайте об отдыхе — и вот вы уже готовы разбирать пример с поиском авиабилетов.
В вашем распоряжении датафрейм с информацией об авиабилетах. Указаны пункты вылета From
и прилёта To
, наличие багажа Has_luggage
, прямой ли рейс Is_Direct
, цена билета туда-обратно Price
, даты вылета Date_From
и прилёта Date_To
, название авиалиний Airline
, время в пути в минутах туда Travel_time_from
и обратно Travel_time_to
.
df = pd.DataFrame(
{
'From': [
'Moscow',
'Moscow',
'St. Petersburg',
'St. Petersburg',
'St. Petersburg',
],
'To': ['Rome', 'Rome', 'Rome', 'Barcelona', 'Barcelona'],
'Is_Direct': [False, True, False, False, True],
'Has_luggage': [True, False, False, True, False],
'Price': [21032, 19250, 19301, 20168, 31425],
'Date_From': [
'01.07.19',
'01.07.19',
'04.07.2019',
'03.07.2019',
'05.07.2019',
],
'Date_To': [
'07.07.19',
'07.07.19',
'10.07.2019',
'09.07.2019',
'11.07.2019',
],
'Airline': ['Belavia', 'S7', 'Finnair', 'Swiss', 'Rossiya'],
'Travel_time_from': [995, 230, 605, 365, 255],
'Travel_time_to': [350, 225, 720, 355, 250],
}
)
Изучим только рейсы из Москвы. Нужен код, работающий как фильтр. Он берёт в будущий срез строки со значением Moscow
в столбце From
, а лишние строки со значением St. Petersburg
не возьмёт.
Этим фильтром может стать булев массив, состоящий из True
и False
. Так, значениями True
отметим, что хотим включить строку c Moscow
в срез данных. А False
обозначим ненужные для среза строки.
Составим булев массив и назовём его filter_list:
filter_list = [True, True, False, False, False]
Он сообщает, что последние три строки не понадобятся, а первые две надо взять. Чтобы автоматически собрать данные в срез, передадим массив как индекс датафрейма:
df[filter_list]
Однако размечать нужные строки вручную — занятие утомительное. Построим фильтр автоматически. Сперва соберём список, где строкам с Moscow
соответствуют значения True
, а остальным — False
.
print(df['From'] == 'Moscow')
1 True
2 False
3 False
4 False
Name: From, dtype: bool
True
:print(df[df['From'] == 'Moscow']) # передаём булев массив как индекс датафрейма
From To Is_Direct Has_luggage Price Date_From Date_To Airline \
0 Moscow Rome False True 21032 01.07.19 07.07.19 Belavia
1 Moscow Rome True False 19250 01.07.19 07.07.19 S7
Travel_time_from Travel_time_to
0 995 350
1 230 225
Отлично! Вы узнали, как задавать условия для получения срезов. Приём одинаково эффективен для датафреймов в 5, 10, 200 и 1000000 строк. Условием создания булева массива может выступать не только равенство, но и другие операции сравнения: !=
, >
, >=
, <
, <=
.
Бюджет на отпуск даже у аналитиков ограничен. Выведем срез данных, где цена на билет меньше 21000 рублей:
df[df['Price'] < 21000]
Значения в столбцах можно сравнивать и с числами, и между собой. Бывает, что путь туда занимает не столько же времени, сколько путь обратно. Рассмотрим случаи, когда на дорогу назад Travel_time_to
уходит больше времени, чем на дорогу вперёд Travel_time_from
:
df[df['Travel_time_to'] > df['Travel_time_from']]
Travel_time_to
более чем в 1.5 раза быстрее, чем дорога туда Travel_time_from
. df[1.5 * df['Travel_time_to'] < df['Travel_time_from']]
Чтобы проверить наличие конкретных значений в столбце, вызовем метод isin()
. Посмотрим, какие рейсы вылетают после 3 июля 2019:
# находим элементы столбца Date_From, равные 4 или 5 июля
df[df['Date_From'].isin(['04.07.2019', '05.07.2019'])]
Иногда нужно получить выборку, соответствующую сразу нескольким условиям, — для этого существуют логические операции pandas. Их синтаксис:
Name | Описание | Синтаксис | Оператор |
---|---|---|---|
И | Результат выполнения логической операции True, только если оба условия — True | (df['Is_Direct']) & (df['Price'] < 21000) | & |
ИЛИ | Результат выполнения — True, если хотя бы одно из условий — True | (df['Has_luggage']) | (df['Price'] < 20000) | | |
НЕ | Результат выполнения — True, если условие — False | ((df['Is_Direct']) ~ (df['Has_luggage'])) | ~ |
Оператор ~
можно использовать и для проверки одного условия, например, чтобы отобрать билеты без багажа: ~(df['Has_luggage'])
.
Обратите внимание, что здесь условия указывают в скобках — в отличие от синтаксиса логических операций Python с or
, and
или not
.
Задача №1/3
Выберите строки с выгодной ценой за авиабилет. Выгодными считаются те билеты, которые дешевле самого дорогого билета более чем в 1,5 раза.
Выведите на экран полученную выборку.
import pandas as pd
df = pd.DataFrame(
{
'From': [
'Moscow',
'Moscow',
'St. Petersburg',
'St. Petersburg',
'St. Petersburg',
],
'To': ['Rome', 'Rome', 'Rome', 'Barcelona', 'Barcelona'],
'Is_Direct': [False, True, False, False, True],
'Has_luggage': [True, False, False, True, False],
'Price': [21032, 19250, 19301, 20168, 31425],
'Date_From': [
'01.07.19',
'01.07.19',
'04.07.2019',
'03.07.2019',
'05.07.2019',
],
'Date_To': [
'07.07.19',
'07.07.19',
'10.07.2019',
'09.07.2019',
'11.07.2019',
],
'Airline': ['Belavia', 'S7', 'Finnair', 'Swiss', 'Rossiya'],
'Travel_time_from': [995, 230, 605, 365, 255],
'Travel_time_to': [350, 225, 720, 355, 250],
}
)
print(df[df['Price'].max()/1.5 > df['Price']]) # впишите нужное условие
From To ... Travel_time_from Travel_time_to
1 Moscow Rome ... 230 225
2 St. Petersburg Rome ... 605 720
3 St. Petersburg Barcelona ... 365 355
[3 rows x 10 columns]
А вам что милее: посетить «Камп Hоу» в Барселоне или бросить монетку в римский фонтан Треви?
Задача №2/3
Выберите строки, где значения столбца Travel_time_from
больше или равны 365 или значения Travel_time_to
меньше 250. Результат выведите на экран.
import pandas as pd
df = pd.DataFrame(
{
'From': [
'Moscow',
'Moscow',
'St. Petersburg',
'St. Petersburg',
'St. Petersburg',
],
'To': ['Rome', 'Rome', 'Rome', 'Barcelona', 'Barcelona'],
'Is_Direct': [False, True, False, False, True],
'Has_luggage': [True, False, False, True, False],
'Price': [21032, 19250, 19301, 20168, 31425],
'Date_From': [
'01.07.19',
'01.07.19',
'04.07.2019',
'03.07.2019',
'05.07.2019',
],
'Date_To': [
'07.07.19',
'07.07.19',
'10.07.2019',
'09.07.2019',
'11.07.2019',
],
'Airline': ['Belavia', 'S7', 'Finnair', 'Swiss', 'Rossiya'],
'Travel_time_from': [995, 230, 605, 365, 255],
'Travel_time_to': [350, 225, 720, 355, 250],
}
)
print(df[(df['Travel_time_from']>=365) | (df['Travel_time_to']<250)]) # впишите нужное условие
Результат
From To ... Travel_time_from Travel_time_to
0 Moscow Rome ... 995 350
1 Moscow Rome ... 230 225
2 St. Petersburg Rome ... 605 720
3 St. Petersburg Barcelona ... 365 355
[4 rows x 10 columns]
Здесь вам не привычные or, and и not. Условия указывают в скобках.
Задача 3/3
Выберите строки, где:
- полёт с пересадкой;
- прилёт до 8 июля (ни 9, ни 10, ни 11 июля).
Результат выведите на экран.
import pandas as pd
df = pd.DataFrame(
{
'From': [
'Moscow',
'Moscow',
'St. Petersburg',
'St. Petersburg',
'St. Petersburg',
],
'To': ['Rome', 'Rome', 'Rome', 'Barcelona', 'Barcelona'],
'Is_Direct': [False, True, False, False, True],
'Has_luggage': [True, False, False, True, False],
'Price': [21032, 19250, 19301, 20168, 31425],
'Date_From': [
'01.07.19',
'01.07.19',
'04.07.2019',
'03.07.2019',
'05.07.2019',
],
'Date_To': [
'07.07.19',
'07.07.19',
'10.07.2019',
'09.07.2019',
'11.07.2019',
],
'Airline': ['Belavia', 'S7', 'Finnair', 'Swiss', 'Rossiya'],
'Travel_time_from': [995, 230, 605, 365, 255],
'Travel_time_to': [350, 225, 720, 355, 250],
}
)
print(df[(df['Is_Direct']==0) & (df['Date_To']<'08.07.2019')]) # впишите нужное условие
Результат
From To Is_Direct ... Airline Travel_time_from Travel_time_to
0 Moscow Rome False ... Belavia 995 350
[1 rows x 10 columns]
Всё верно. Лишь одна строка удовлетворяет таким строгим условиям.
Срезы данных методом query()
В прошлом уроке вы делали срезы в 2 шага:
- Получали булев массив, соответствующий условиям.
- Делали срез по нему.
Это гибкий инструмент получения срезов, и владеть им полезно. Однако существует и более простой способ — метод query()
(пер. «запрос»).
Необходимое условие для среза записывается в строке, которую передают как аргумент методу query()
. А метод применяют к датафрейму. В результате получаем нужный срез.
import pandas as pd
df = pd.DataFrame(
{
'From': [
'Moscow',
'Moscow',
'St. Petersburg',
'St. Petersburg',
'St. Petersburg',
],
'To': ['Rome', 'Rome', 'Rome', 'Barcelona', 'Barcelona'],
'Is_Direct': [False, True, False, False, True],
'Has_luggage': [True, False, False, True, False],
'Price': [21032, 19250, 19301, 20168, 31425],
'Date_From': [
'01.07.19',
'01.07.19',
'04.07.2019',
'03.07.2019',
'05.07.2019',
],
'Date_To': [
'07.07.19',
'07.07.19',
'10.07.2019',
'09.07.2019',
'11.07.2019',
],
'Airline': ['Belavia', 'S7', 'Finnair', 'Swiss', 'Rossiya'],
'Travel_time_from': [995, 230, 605, 365, 255],
'Travel_time_to': [350, 225, 720, 355, 250],
}
)
print(df.query('To == "Barcelona"'))
From To Is_Direct Has_luggage Price Date_From \
3 St. Petersburg Barcelona False True 20168 03.07.2019
4 St. Petersburg Barcelona True False 31425 05.07.2019
Date_To Airline Travel_time_from Travel_time_to
3 09.07.2019 Swiss 365 355
4 11.07.2019 Rossiya 255 250
Условия, указанные в параметре query()
:
- Поддерживают разные операции сравнения:
!=
,>
,>=
,<
,<=
. - Проверяют, входят ли конкретные значения в список, конструкцией:
Date_To in ["07.07.19", "09.07.2019"]
. Если нужно узнать, нет ли в списке определённых значений, пишут так:Date_To not in ["07.07.19", "09.07.2019"]
. - Работают с логическими операторами в привычном виде, где «или» —
or
, «и» —and
, «не» —not
. Указывать условия в скобках необязательно. Без скобок операции выполняются в следующем порядке: сначалаnot
, потомand
и, наконец,or
.
Обратите внимание, что значение "Barcelona"
в коде задачи заключено в двойные кавычки. Это нужно, чтобы различать одинарные кавычки, оформляющие строку, и кавычки для элемента Barcelona
.
Условия для среза данных можно объединять. Например, чтобы отыскать прямые рейсы или билеты с включённым в стоимость багажом — мало кому нравится летать со множеством пересадок, да ещё и без вещей.
print(df.query('Is_Direct == True or Has_luggage == True'))
Строки с какими индексами не будут выбраны в результате выполнения этого кода?
print(df.query('Is_Direct == True or Has_luggage == True'))
From To Is_Direct Has_luggage Price Date_From Date_To Airline Travel_time_from Travel_time_to
0 Moscow Rome False True 21032 01.07.19 07.07.19 Belavia 995 350
1 Moscow Rome True False 19250 01.07.19 07.07.19 S7 230 225
2 St. Petersburg Rome False False 19301 04.07.2019 10.07.2019 Finnair 605 720
3 St. Petersburg Barcelona False True 20168 04.07.2019 09.07.2019 Swiss 365 355
4 St. Petersburg Barcelona True False 31425 04.07.2019 11.07.2019 Rossiya 255 250
0
1
Правильный ответ
2
Верно! Все дороги ведут в Рим, но перелёт из Санкт-Петербурга с пересадкой и без багажа — не самая приятная из них.
3
4
А как насчёт слетать куда-нибудь вместе с командой Практикума? Правда, есть ограничение: вылететь мы сможем только из Москвы. Добавьте в query()
новое условие. Порядок выполнения трёх и более условий можно регулировать скобками.
df.query('Travel_time_from < Travel_time_to.mean()')
query()
можно включать внешние переменные (не из датафрейма). Когда упоминаете такую переменную, помечайте её знаком @
:Задача 1/2
Выберите строки, где: Has_luggag
равно False
и Airline
не равно ни S7
, ни Rossiya
. Напечатайте полученную выборку на экране.
import pandas as pd
df = pd.DataFrame(
{
'From': [
'Moscow',
'Moscow',
'St. Petersburg',
'St. Petersburg',
'St. Petersburg',
],
'To': ['Rome', 'Rome', 'Rome', 'Barcelona', 'Barcelona'],
'Is_Direct': [False, True, False, False, True],
'Has_luggage': [True, False, False, True, False],
'Price': [21032, 19250, 19301, 20168, 31425],
'Date_From': [
'01.07.19',
'01.07.19',
'04.07.2019',
'03.07.2019',
'05.07.2019',
],
'Date_To': [
'07.07.19',
'07.07.19',
'10.07.2019',
'09.07.2019',
'11.07.2019',
],
'Airline': ['Belavia', 'S7', 'Finnair', 'Swiss', 'Rossiya'],
'Travel_time_from': [995, 230, 605, 365, 255],
'Travel_time_to': [350, 225, 720, 355, 250],
}
)
# впишите условие создания нужной выборки
print(df.query('Has_luggage==False and Airline!="S7" and Airline!="Rossiya"'))
From To Is_Direct ... Airline Travel_time_from Travel_time_to
2 St. Petersburg Rome False ... Finnair 605 720
[1 rows x 10 columns]
Записать запрос в query() легко. Просто проговорите условие:
«Has_luggage равно False» — Has_luggage == False;
«и» — and;
«Airline не равно ни S7, ни Rossiya» — Airline not in ["S7", "Rossiya"].
Задача 2/2
Выберите строки, где Airline
равно Belavia
, S7
или Rossiya
, при этом Travel_time_from
меньше переменной под названием max_time
. Напечатайте полученную выборку на экране.
import pandas as pd
df = pd.DataFrame(
{
'From': [
'Moscow',
'Moscow',
'St. Petersburg',
'St. Petersburg',
'St. Petersburg',
],
'To': ['Rome', 'Rome', 'Rome', 'Barcelona', 'Barcelona'],
'Is_Direct': [False, True, False, False, True],
'Has_luggage': [True, False, False, True, False],
'Price': [21032, 19250, 19301, 20168, 31425],
'Date_From': [
'01.07.19',
'01.07.19',
'04.07.2019',
'03.07.2019',
'05.07.2019',
],
'Date_To': [
'07.07.19',
'07.07.19',
'10.07.2019',
'09.07.2019',
'11.07.2019',
],
'Airline': ['Belavia', 'S7', 'Finnair', 'Swiss', 'Rossiya'],
'Travel_time_from': [995, 230, 605, 365, 255],
'Travel_time_to': [350, 225, 720, 355, 250],
}
)
max_time = 300
# впишите условие создания нужной выборки
print(df.query('(Travel_time_from < @max_time) and (Airline=="Belavia" or Airline=="S7" or Airline=="Rossiya") '))
Результат
From To ... Travel_time_from Travel_time_to
1 Moscow Rome ... 230 225
4 St. Petersburg Barcelona ... 255 250
[2 rows x 10 columns]
Вы уже умеете строить срезы данных целыми двумя способами. Не пора ли применить эту магию к данным по АЗС?
Есть несколько заездов со временем около 30 000 секунд (это около 8 часов). Крайне маловероятно, что водители заправляются так долго. Они повстречали свою любовь или случайно вырвали заправочный пистолет? А может, всё одновременно?
У вас есть всё необходимое, чтобы с этим разобраться.
Чтобы построить графики, обращаются к библиотеке matplotli
(от англ. mathematical plotting library, «библиотека математических построений»). Точнее, к её модулю — pyplot
:
import matplotlib.pyplot as plt
show()
. Он позволяет посмотреть, как отличаются гистограммы с разным числом корзин:import matplotlib.pyplot as plt
data.hist(bins=10)
plt.show()
data.hist(bins=100)
plt.show()
Задача 1/3
Итак, нужно разобраться с аномалиями в выборке. Для начала найдите АЗС с самыми большими значениями в столбце time_spent
.
Одной строкой кода отсортируйте таблицу по убыванию значений в столбце time_spent
и выведите на экран первые 10 строк всей таблицы.
import pandas as pd
data = pd.read_csv('/datasets/visits.csv', sep='\t')
print(data.sort_values(by='time_spent', ascending=False).head(10))
Результат
date_time id time_spent name
114797 20180402T055708 3c1e4c52 28925.0 Василёк
27147 20180406T080254 4b5f2af5 28519.0 Немезия
60547 20180408T000002 cf1ba8a5 28292.0 Василёк
19042 20180408T204208 5410e876 23696.0 Василёк
118597 20180408T165020 3c1e4c52 21184.0 Василёк
118058 20180402T111333 3c1e4c52 20359.0 Василёк
114406 20180408T083722 3c1e4c52 19886.0 Василёк
132164 20180405T160745 627ea5e3 19445.0 Левкой
281360 20180406T180459 d0c0928d 18614.0 Пион
165326 20180402T230204 3af3bb71 18569.0 Агератум
Во-первых, в столбце id разные значения, значит, долго заправляются не на одной-единственной АЗС. Во-вторых, АЗС в списке вообще из разных сетей.
Выделяется id == "3c1e4c52", он встречается несколько раз. Изучите эту АЗС детальнее.
Задача 2/3
Четыре из десяти самых долгих заездов произошли на станции под номером 3c1e4c52
. Аналитик данных непременно спросит: «А как распределение времени, проведённого на этой АЗС, соотносится с распределением времени заездов в целом?» Нужно проверить. Для этого сделайте срез данных и извлеките всю информацию о станции 3c1e4c52
.
- Сделайте срез
data
по АЗС сid == "3c1e4c52"
и сохраните результат в переменнуюsample
. - Выведите на экран число заездов на эту АЗС.
import pandas as pd
data = pd.read_csv('/datasets/visits.csv', sep='\t')
sample = data.query('id == "3c1e4c52"')
print(sample['date_time'].count())
5814
На АЗС под кодовым названием 3c1e4c52 заезжали не раз.
Задача №3/3
Нужно сравнить распределение времени пребывания на станции 3c1e4c52
с распределением времени пребывания на всех АЗС. Если они сильно различаются, возможно, станция 3c1e4c52
представляет собой статистический выброс.
- Методом
hist()
постройте две гистограммы распределения значений в столбцеtime_spent
: одну для объектаdata
, вторую — дляsample
. Не забудьте использоватьplt.show()
после каждого вызоваhist()
. Для обеих гистограмм задайте одинаковые аргументы:range
— от 0 до 1500,bins
— 100.
import pandas as pd
import matplotlib.pyplot as plt
data = pd.read_csv('/datasets/visits.csv', sep='\t')
sample = data.query('id == "3c1e4c52"')
data['time_spent'].hist(range=(0,1500), bins=100)
plt.show()
sample['time_spent'].hist(range=(0,1500), bins=100)
plt.show()
)
Гистограммы среза sample
и всего набора данных внешне похожи — имеют по два пика: около нулевого времени проезда и около 200 секунд. Только вот гистограмма sample
не такая гладкая, на ней заметны одиночные хаотичные всплески или шумы. Общее правило: чем данных меньше, тем шумнее гистограмма.
«Слишком долгая» заправка — это сколько?
Нетипично долгие заезды признаем выбросами и отбросим. Почему так можно?
Во-первых, их немного. Во-вторых, скорее всего, такие заезды — не заправки. Например, водитель мог отдыхать, есть или заниматься чем-то ещё. Раз это не заправки в чистом виде, значит, в рамках нашего исследования такие заезды не интересны.
Как определить, что заправка «слишком долгая»? Где провести границу между заправкой автомобиля и прочими занятиями на АЗС?
Посмотрите на гистограмму:
Заправки продолжительностью 600 секунд — уже редки. Дольше 800 секунд почти не заправляются. А на участке более 1300 секунд гистограмма сливается с нулём (это не значит, что там ровно 0, но таких заправок единицы).
Примем верхнюю границу в 1000 секунд. Это число кажется разумным: дольше заправляются редко. Если отбросить значения больше 1000, много данных не потеряется. Да и вряд ли водители тратят на заправку больше 1000 секунд (16 минут). Решено. Продолжаем работать с наблюдениями, удовлетворяющими условию: data.query('time_spent < 1000')
.
Работа с датой и временем
Мы посчитали подозрительно долгие заправки выбросами. Но что делать с чрезвычайно короткими заправками? Изучим их подробнее. Узнаем, связана ли их продолжительность со временем заезда на АЗС.
data.head()
В столбце date_time
дата и время заезда. Из описания данных известно, что время заезда указали в часовом поясе UTC+0, в формате ISO. Значит, сначала слитно идут год, месяц, день; затем буквенный разделитель даты и времени T; затем часы, минуты и секунды — снова слитно.
В курсе по предобработке вы познакомились с методом to_datetime()
, который переводит строки в даты. Напомним, что в аргументе format
метода to_datetime()
указывают специальные обозначения, порядок которых соответствует порядку чисел в строке с датой:
%d
— день месяца (от 01 до 31);%m
— номер месяца (от 01 до 12);%Y
— четырёхзначный номер года (например, 2019);%y
— двузначный номер года (например, 19);Z
илиT
— стандартный разделитель даты и времени;%H
— номер часа в 24-часовом формате;%I
— номер часа в 12-часовом формате;%M
— минуты (от 00 до 59);%S
— секунды (от 00 до 59).
При выводе значений формата datetime
на экран Python автоматически разделяет их символами -
и :
, чтобы человеку было проще воспринимать данные.
Пора научиться:
- округлять даты;
- добывать отдельные компоненты из дат, например день недели;
- «сдвигать» даты в другие часовые пояса.
О том, что операции предстоит выполнять именно с датами, аналитик сообщает pandas отдельно, через атрибут dt
(от англ. date time). Атрибут dt
указывает, что тип данных, к которым будут применены методы, — datetime
. А значит, pandas не примет их за строки или числа.
Чтобы округлить время, применяют метод dt.round()
(англ. round, «округлять»). В качестве параметра ему передают строку с шагом округления в часах, днях, минутах или секундах:
D
— day (от англ. «день»);H
— hour (от англ. «час»);min
илиT
— minute (от англ. «минута»);S
— second (от англ. «секунда»).
Чаще всего округляют с шагом в один час:
import pandas as pd
df = pd.DataFrame({'time': ['2011-03-01 17:34']})
df['time'] = pd.to_datetime(df['time'], format='%Y-%m-%d %H:%M')
df['time_rounded'] = df['time'].dt.round('1H') # округляем до ближайшего значения с шагом в один час
print(df['time_rounded'])
0 2011-03-01 18:00:00
Name: time, dtype: datetime64[ns]
dt.round()
округляет до ближайшего значения — не всегда получается в бóльшую сторону. Четверть шестого после округления методом dt.round()
станет пятью часами:
import pandas as pd
df = pd.DataFrame({'time': ['2011-03-01 17:15']})
df['time'] = pd.to_datetime(df['time'], format='%Y-%m-%d %H:%M')
# округляем до ближайшего значения с шагом в один час
df['time_rounded'] = df['time'].dt.round('1H')
print(df['time_rounded'])
0 2011-03-01 17:00:00
Name: time_rounded, dtype: datetime64[ns]
Чтобы быть уверенными в том, что время будет округлено к большему значению, обращаются к методу dt.ceil()
(от англ. ceiling — «потолок»). К меньшему значению, «вниз», округляют методом dt.floor()
(англ. floor — «пол»).
import pandas as pd
df = pd.DataFrame({'time': ['2011-03-01 17:15']})
df['time'] = pd.to_datetime(df['time'], format='%Y-%m-%d %H:%M')
df['ceil'] = df['time'].dt.ceil('1H') # округляем к потолку
df['floor'] = df['time'].dt.floor('1H') # округляем к полу
print('Время, округлённое вверх', df['ceil'])
print('Время, округлённое вниз', df['floor'])
Время, округлённое вверх 0 2011-03-01 18:00:00
Name: time, dtype: datetime64[ns]
Время, округлённое вниз 0 2011-03-01 17:00:00
Name: time, dtype: datetime64[ns]
Номер дня в неделе находят методом dt.weekday
(англ. weekday — «будний день»). Понедельник — день под номером 0, а воскресенье — шестой день.
import pandas as pd
df = pd.DataFrame({'time': ['2011-03-07 17:15', '2011-04-02 17:15']}) # пн и сб
df['time'] = pd.to_datetime(df['time'], format='%Y-%m-%d %H:%M')
df['weekday'] = df['time'].dt.weekday
print(df['weekday'])
0 0
1 5
Name: time, dtype: int64
Иногда нужно переводить время в другой часовой пояс. За временные сдвиги отвечает pd.Timedelta()
(от англ. time delta — «дельта времени, перепад во времени»). Количество часов передают в параметре: (hours=)
.
Прибавим 9 часов к московскому времени и узнаем, который час был в Петропавловске-Камчатском, когда в Москве происходили события датафрейма:
import pandas as pd
df = pd.DataFrame({'time': ['11-03-07 17:15', '11-05-02 10:20']})
df['moscow_time'] = pd.to_datetime(df['time'], format='%y-%m-%d %H:%M')
df['petropavlovsk-kamchatsky_time'] = df['moscow_time'] + pd.Timedelta(hours=9)
print(df['petropavlovsk-kamchatsky_time'])
1 2011-05-02 19:20:00
Name: petropavlovsk-kamchatsky_time, dtype: datetime64[ns]
Можно и наоборот: отнимать время, указав в параметре отрицательное количество часов.
Задача №1/3
Причиной коротких заездов может быть то, что водители нечаянно заезжают на АЗС, которые не работают по ночам. Если это действительно так, то вы увидите связь между короткими заездами и временем прибытия. Чтобы проверить эту гипотезу, измените тип столбца date_time
на более удобный тип для даты.
- Методом
pd.to_datetime()
переведите значения столбцаdate_time
в таблицеdata
в объектыdatetime
. В параметреformat=
укажите строку, соответствующую текущему форматуdate_time
, с помощью специальных обозначений. - Выведите на экран первые пять строк data, чтобы проверить, что получилось.
import pandas as pd
data = pd.read_csv('/datasets/visits.csv', sep='\t')
data['date_time'] = pd.to_datetime(data['date_time'],format='%Y%m%dT%H%M%S')
print(data.head(5))
Результат
date_time id time_spent name
0 2018-04-06 16:53:58 76144fb2 98.0 Василёк
1 2018-04-04 17:39:13 76144fb2 15.0 Василёк
2 2018-04-03 17:28:24 76144fb2 220.0 Василёк
3 2018-04-07 07:04:41 76144fb2 19.0 Василёк
4 2018-04-04 13:20:49 76144fb2 14.0 Василёк
Наконец-то дата человеко понятна. И сердцу мила. И внешне приятна.
Задача 2/3
Напомним, что в датафрейме записано время UTC. Московское рассчитывают как UTC + 3 часа. Создайте столбец data['local_time']
и сохраните в нём сдвинутое на 3 часа время из столбца data['date_time']
. Напечатайте первые 5 строк таблицы data.
import pandas as pd
data = pd.read_csv('/datasets/visits.csv', sep='\t')
data['date_time'] = pd.to_datetime(data['date_time'],format='%Y%m%dT%H%M%S')
#print(data.head(5))
data['local_time'] = data['date_time'] + pd.Timedelta(hours=3)
print(data.head(5))
date_time id time_spent name local_time
0 2018-04-06 16:53:58 76144fb2 98.0 Василёк 2018-04-06 19:53:58
1 2018-04-04 17:39:13 76144fb2 15.0 Василёк 2018-04-04 20:39:13
2 2018-04-03 17:28:24 76144fb2 220.0 Василёк 2018-04-03 20:28:24
3 2018-04-07 07:04:41 76144fb2 19.0 Василёк 2018-04-07 10:04:41
4 2018-04-04 13:20:49 76144fb2 14.0 Василёк 2018-04-04 16:20:49
В UTC — 12 часов. В Москве — 15 часов. В Петропавловске-Камчатском — полночь.
Задача 3/3
Данные, связанные со временем, лучше округлять до той величины, которой будет достаточно для детального анализа. Чтобы проанализировать взаимосвязь между временем прибытия на АЗС и продолжительностью заезда, точность до минут и секунд не нужна. Округлите время до часов.
Выполните следующие шаги:
- Создайте новый столбец
date_hou
r и передайте ему значения столбцаlocal_time
, округлённые до часов. - Выведите первые пять строк
data
, чтобы проверить результаты.
import pandas as pd
data = pd.read_csv('/datasets/visits.csv', sep='\t')
data['date_time'] = pd.to_datetime(data['date_time'],format='%Y%m%dT%H%M%S')
#print(data.head(5))
data['local_time'] = data['date_time'] + pd.Timedelta(hours=3)
#print(data.head(5))
data['date_hour'] = data['local_time'].dt.round('1H')
print(data.head(5))
Результат
date_time id ... local_time date_hour
0 2018-04-06 16:53:58 76144fb2 ... 2018-04-06 19:53:58 2018-04-06 20:00:00
1 2018-04-04 17:39:13 76144fb2 ... 2018-04-04 20:39:13 2018-04-04 21:00:00
2 2018-04-03 17:28:24 76144fb2 ... 2018-04-03 20:28:24 2018-04-03 20:00:00
3 2018-04-07 07:04:41 76144fb2 ... 2018-04-07 10:04:41 2018-04-07 10:00:00
4 2018-04-04 13:20:49 76144fb2 ... 2018-04-04 16:20:49 2018-04-04 16:00:00
[5 rows x 6 columns]
Вы привели время к рабочему формату. Новые столбцы local_time и date_hour пригодятся уже в следующем уроке. Не переключайтесь!
Графики
Что делать, если данных много, а построить график хочется? Разглядывать сотни тысяч точек сложно. Значит, визуализировать нужно сгруппированные данные или срез. В этом уроке вы построите график по готовому срезу id == '3c1e4c52'
. Начнём с основ.
За построение графиков в pandas отвечает метод plot()
(пер. «график»). Вот простой пример:
df = pd.DataFrame({'a': [2, 3, 4, 5], 'b': [4, 9, 16, 25]})
print(df)
df.plot()
a b
0 2 4
1 3 9
2 4 16
3 5 25
Метод plot()
построил графики по значениям столбцов из датафрейма. На оси абсцисс (x) расположились индексы, а на оси ординат (y) — значения столбцов. Названия для графиков указывают строкой или переменной в параметре title
(англ. «название»):
df.plot(title='A и B')
Элементов в таблице слишком мало, чтобы они складывались в непрерывную линию. Добавим графику точности, передадим параметр style
, со значением o
, чтобы отметить значения таблицы точками.
df.plot(style='o') # 'o' похожа на кружок или точку, запомнить легко
Можно задать и другую форму точек. Например, style='х'
пометит точки крестиками:
df.plot(style='x') # 'x' - точь-в-точь крестик`r
Когда нужен компромиссный вариант — и линии, и точки, — передают style='o-'
.
df.plot(style='o-') # 'o-' - кружок и линия
Напомним, что по горизонтальной оси отложены индексы. Но что, если такой способ представления не годится для анализа? Можно изменить сами индексы или передать методу plot()
параметры осей. Так, оси абсцисс (x) присвоим значения столбца b
, а оси ординат (y) — значения столбца a
:
df.plot(x='b', y='a', style='o-')
По оси абсцисс идут значения столбца b
, а по оси ординат — значения столбца a
. Обратите внимание, что pandas переименовал горизонтальную ось: теперь она — b
. А в легенде (списке условных обозначений на графике) осталась только линия со значениями столбца a
.
Ещё не всё идеально: точки упираются в края графика. Скорректируем границы параметрами xlim
и ylim
— с ними вы познакомились, когда изучали ящик с усами. Напомним, что параметрам xlim
и ylim
в скобках передают минимальное и максимальное значение. Ограничим ось абсцисс значениями от 0 до 30:
df.plot(x='b', y='a', style='o-', xlim=(0, 30))
Добавим линии сетки: с ними будет легче понять, какие именно значения отображены. Укажем параметр grid
(пер. «сетка, решётка»), равный True
(это значит, что отображать сетку — нужно):
df.plot(x='b', y='a', style='o-', xlim=(0, 30), grid=True)
Размером графика управляют через параметр figsize
(от англ. size of a figure — «размер фигуры»). Ширину и высоту области построения в дюймах передают параметру в скобках: figsize = (x_size, y_size)
. Сравним графики с разными размерами:
# строим маленький график
df.plot(x='b', y='a', style='o-', xlim=(0, 30), grid=True, figsize=(1, 1))
# строим большой график
df.plot(x='b', y='a', style='o-', xlim=(0, 30), grid=True, figsize=(10, 3))
Вот вы и знаете о построении графиков так много, что справитесь с визуальным представлением срезов данных по АЗС.
Задача
Снова создайте переменную sample
, записав в неё срез из данных по АЗС с id == '3c1e4c52'
. Обратите внимание, что на этот раз в sample
войдут все форматы времени.
Пользуясь данными sample
, постройте график зависимости продолжительности заправки от времени заезда. За основу возьмите соответствующие столбцы time_spent
и local_time
. Оси X присвойте значения столбца local_time
, а оси Y — значения столбца time_spent
.
Проверьте, всё ли верно отображено на графике:
- Каждый элемент обозначен точкой.
- Диапазон оси Y указан от 0 до 1000.
- Добавлены сетки.
- Размер графика 12х6 дюймов.
import pandas as pd
data = pd.read_csv('/datasets/visits.csv', sep='\t')
data['date_time'] = pd.to_datetime(
data['date_time'], format='%Y-%m-%dT%H:%M:%S'
)
data['local_time'] = data['date_time'] + pd.Timedelta(hours=3)
sample = data.query('id == "3c1e4c52"')
sample.plot(x='local_time', y='time_spent', style='o', ylim=(0, 1000), grid=True, figsize=(12, 6))
Результат
(
Закономерность прослеживается, только она никак не связана с короткими заездами. Оказывается, такие заезды бывают в любое время. Поэтому теперь нужно понять, стоит ли вообще исключать эти данные.
Группировка с pivot_table()
На графике видна структура с провалами плотности в районе ночных часов. Это кажется правдоподобным: машин и, как следствие, заправок ночью меньше.
Подозрительно коротких заездов (с продолжительностью менее 100 секунд) много в любое время суток. Хотя график строили по срезу, данных будто не стало меньше. Точки сливаются, делать по ним уверенные выводы трудно. Попробуем улучшить визуальное представление группировкой данных.
Прежде чем строить красивый график, позаботимся о красивом коде. В прошлом уроке вы сохранили срез в переменной sample
и к ней применили метод plot()
:
sample = data.query('id == "3c1e4c52"')
sample.plot(
x='local_time',
y='time_spent',
ylim=(0, 1000),
style='o',
grid=True,
figsize=(12, 6),
)
Такой код хорош, когда к sample
обращаются и дальше, в будущих расчётах. А если это не так? Введение новой сущности ради построения графика путает больше, чем упрощает.
БРИТВА ОККАМА Entia non sunt multiplicanda praeter necessitatem — «Не следует множить сущности без необходимости». Фраза раскрывает принцип, названный «Бритва Оккама» в честь философа XIII века, монаха Уильяма из английской деревушки Оккам.
Суть принципа: совершенство должно быть простым.
Если какого-то результата можно достичь с привлечением сущностей A, B и C либо другим путём с привлечением A, B, С и D — надо выбирать первый путь.
Не будем множить сущности без необходимости и избавимся от промежуточных переменных. Применим метод plot(
) к результату работы query()
без всяких sample
. Получится конструкция вида:
data.query().plot()
Передадим нужные параметры. Чтобы код было легко читать, запишем его в несколько строк. Так код выглядит яснее:
(data
.query('id == "3c1e4c52"')
.plot(x='local_time', y='time_spent',
ylim=(0, 1000), style='o', grid=True, figsize=(12, 6))
)
# одна команда в несколько строк: не забыть заключить конструкцию в скобки
Вернёмся к данным по АЗС. На графике слишком много точек. Чтобы сделать его нагляднее, будем отмечать не визит, а среднюю продолжительность заправки в час. Вы уже создавали столбец date_hour
с округлённым временем заезда на АЗС.
Обратимся к pivot_table()
. Добавим сводную таблицу в цепочку между query()
и plot()
:
data['date_time'] = pd.to_datetime(
data['date_time'], format='%Y-%m-%dT%H:%M:%S'
)
data['local_time'] = data['date_time'] + pd.Timedelta(hours=3)
data['date_hour'] = data['local_time'].dt.round('1H')
(
data.query('id == "3c1e4c52"')
.pivot_table(index='date_hour', values='time_spent')
.plot(grid=True, figsize=(12, 5))
)
На графике показана средняя продолжительность заправки на АЗС под номером 3c1e4c52
за каждый час. Обратите внимание, что в среднем водители проводят на АЗС около 500 секунд. Помня о пике гистограммы в 200 секунд, нельзя не удивиться такому значению.
Есть и таинственный пик, когда среднее время заправки достигло 3000 секунд (почти час). Многовато. В прошлых уроках заезды дольше 1000 секунд вообще отбрасывались, ведь они не слишком похожи на правду. Добавим соответствующее условие в query()
:
(
data.query('id == "3c1e4c52" and time_spent < 1000')
.pivot_table(index='date_hour', values='time_spent')
.plot(grid=True, figsize=(12, 5))
)
Средняя продолжительность заправки снизилась вполовину! Вот вы и увидели, как выбросы влияют на среднее.
Вычислим устойчивую к выбросам медиану. В pivot_table()
значением параметра aggfunc
передадим median
.
Напомним, что в aggfunc
передают функцию, которую применяют к значениям сводной таблицы. Например, метод count
посчитает число значений в группе. Если в aggfunc не указать ничего, отработает расчёт среднего: `mean.
(
data.query('id == "3c1e4c52"')
.pivot_table(index='date_hour', values='time_spent', aggfunc='median')
.plot(grid=True, figsize=(12, 5))
)
Обратите внимание, что даже без фильтра time_spent < 1000
медиана дала среднюю продолжительность заправки примерно в 200 секунд. Медиана устойчива к выбросам, но всё же не безупречна: пик более 800 секунд в ночь со 2 на 3 апреля выглядит аномальным значением.
В целом нет никакой явной связи между продолжительностью заезда и временем дня. Это странно. На этой АЗС не бывает очередей? Или что-то не так с данными? Число заездов в течение дня точно должно меняться. Проверьте это.
Задача
Если между временем прибытия на АЗС и числом заездов нет никакой связи, это серьёзный повод насторожиться. Вряд ли количество заездов в два часа ночи и в восемь утра одинаково. Чтобы понять, что же происходит, постройте график зависимости между временем прибытия и количеством заездов в час.
Выполните следующие шаги, помня о бритве Оккама:
- Сделайте срез из
data
по АЗС сid=="3c1e4c52"
. - Из данных этого среза постройте сводную таблицу, которая будет отображать количество заездов по времени прибытия. Передайте параметру
values
значение'time_spent'
. - Из данных сводной таблицы постройте график зависимости между временем прибытия и количеством заездов в час (по аналогии с примером в уроке). Добавьте линии сетки, задайте размер графика 12х5 дюймов.
import pandas as pd
data = pd.read_csv('/datasets/visits.csv', sep='\t')
data['local_time'] = pd.to_datetime(
data['date_time'], format='%Y-%m-%dT%H:%M:%S'
) + pd.Timedelta(hours=3)
data['date_hour'] = data['local_time'].dt.round('1H')
(
data.query('id == "3c1e4c52"')
.pivot_table(index='date_hour', values='time_spent', aggfunc='count')
.plot(grid=True, figsize=(12, 5))
)
Результат
Наконец-то вы обнаружили ожидаемую закономерность! Ночью заездов на АЗС в несколько раз меньше, чем днём. Факт заезда в данных отображён правдоподобно. А вот с продолжительностью пока непонятно.
Помечаем срез данных
Продолжительность заправки около 0 секунд всё ещё выглядит необъяснимо странной. Неужели такие заезды придётся отбросить? Это уже было сделано с подозрительно долгими заправками, признанными выбросами. Однако сверхкоротких заездов гораздо больше, чем очень долгих. Если от них избавиться, то данные наверняка сильно исказятся.
Как много строк потеряется, если просто отбросить подозрительно короткие события? Посчитаем долю заездов на АЗС продолжительностью менее 60 секунд:
# делим количество заездов короче 60 секунд на общее число заездов
print(len(data.query('time_spent < 60')) / len(data))
0.42213910893586964
Таких аномально быстрых заездов 42%. Очень много.
Насколько равномерно они распределены по разным АЗС? Везде около 40%? Или где-то их нет, а на каких-то АЗС много? Выясните, откуда взялось такое среднее. Найдите для каждой АЗС среднее число аномально быстрых заездов. Пометьте их и сгруппируйте данные по АЗС. И по этим значениям уже постройте гистограмму.
Задача 1/5
Первым делом нужно создать переменную, чтобы выделить аномально быстрые заезды. Добавьте в таблицу data столбец too_fast (пер. «слишком быстрый») со значениями: True — если продолжительность заезда из столбца time_spent менее 60 секунд. False — все остальные значения. Затем выведите на экран первые пять строк таблицы data, чтобы проверить новый столбец.
import pandas as pd
data = pd.read_csv('/datasets/visits.csv', sep='\t')
data['too_fast'] = data['time_spent'] < 60
print(data.head(5))
Результат
\ | date_time | id | time_spent | name | too_fast |
---|---|---|---|---|---|
0 | 20180406T165358 | 76144fb2 | 98.0 | Василёк | False |
1 | 20180404T173913 | 76144fb2 | 15.0 | Василёк | True |
2 | 20180403T172824 | 76144fb2 | 220.0 | Василёк | False |
3 | 20180407T070441 | 76144fb2 | 19.0 | Василёк | True |
4 | 20180404T132049 | 76144fb2 | 4.0 | Василёк | True |
Вот и знакомый булев массив! Кстати, вы никогда не задумывались, что получится, если рассчитать среднее арифметическое по такому массиву?
Задача 2/5
Рассчитать процент всех заездов короче 60 секунд можно разными способами. Можно посчитать значения True
в столбце too_fast
методом value_counts()
и разделить получившееся число на количество строк.
Другой способ — применить к столбцу too_fast
метод mean()
. Ведь среднее рассчитывают так: сумму значений делят на количество значений. Если применить арифметическую операцию к булевым значениям True
и False
, значение True будет интерпретировано как 1, а False — как 0. С помощью mean()
можно сделать оба вычисления сразу: посчитать True
и разделить его на количество строк.
Таким образом, найти процент быстрых заездов можно с помощью среднего арифметического.
Рассчитайте среднее арифметическое для значений в столбце too_fast
и выведите его на экран.
import pandas as pd
data = pd.read_csv('/datasets/visits.csv', sep='\t')
data['too_fast'] = data['time_spent'] < 60
print(data['too_fast'].mean())
0.42213910893586964
Теперь вы знаете, что процент быстрых заездов можно рассчитать вызовом mean()
для булевых переменных: просто и удобно.
Задача 3/5
Переменная задана, процент посчитан, теперь можно группировать данные по АЗС. Для этого воспользуйтесь сводной таблицей.
- Создайте переменную
too_fast_stat
и запишите в неё значения из сводной таблицы, сгруппировав доли быстрых заездов по АЗС. - Выведите на экран первые пять строк
too_fast_stat
.
import pandas as pd
data = pd.read_csv('/datasets/visits.csv', sep='\t')
data['too_fast'] = data['time_spent'] < 60
too_fast_stat = data.pivot_table(index='id', values='too_fast', aggfunc='mean')
print(too_fast_stat.head(5))
Результат
too_fast
id
00ca1b70 0.250000
011f7462 0.637489
015eaddd 0.726190
0178ce70 0.211538
018a83ef 0.510269
Уже по первым пяти строкам видно, что доля сверхкоротких заправок очень неоднородна: на одних АЗС они составляют по 20% от всех заездов, а на других — целых 70%.
Задача 4/5
Теперь вы знаете, сколько быстрых заездов на первых пяти АЗС в процентном отношении. Но что делать дальше — выводить на экран остальные 466 строк и изучать значения для каждой АЗС? Слишком сложно.
Гораздо лучше визуализировать распределение быстрых заездов сразу по всем АЗС. Гистограмма, вот что нужно!
Постройте гистограмму распределения значений в таблице too_fast_stat
на 30 корзин.
import pandas as pd
data = pd.read_csv('/datasets/visits.csv', sep='\t')
data['too_fast'] = data['time_spent'] < 60
too_fast_stat = data.pivot_table(index='id', values='too_fast')
too_fast_stat.hist(bins=30)
Результат
Пик графика около 0.3. Значит, у большинства АЗС около 30% заездов аномально быстрые. Однако бывают и АЗС, где 100% заездов аномально быстрые! Эту проблему нужно зафиксировать и сообщить о ней коллегам, готовившим данные к анализу.
Задача 5/5
Теперь, когда вы разобрались, как использовать булевы значения для подсчёта процентов, примените этот метод для аномально долгих заправок — проверьте их распределение по АЗС. Как вы помните, заезды длиннее 1000 секунд решили исключить. Сейчас станет понятно, сколько АЗС это затронет.
- Добавьте в
data
столбецtoo_slow
(пер. «слишком медленный»), в котором значения из столбцаtime_spent
больше 1000 секунд будут отмечены какTrue
, а все остальные — какFalse
.
Помня о бритве Оккама:
- Создайте сводную таблицу с процентом медленных заездов для каждой АЗС.
- Постройте гистограмму доли медленных заездов по всем АЗС на 30 корзин.
import pandas as pd
data = pd.read_csv('/datasets/visits.csv', sep='\t')
data['too_fast'] = data['time_spent'] < 60
too_fast_stat = data.pivot_table(index='id', values='too_fast')
too_fast_stat.hist(bins=30)
data['too_slow'] = data['time_spent'] > 1000
(
data
.pivot_table(index='id', values='too_slow')
.hist(bins=30)
)
Результат
Слишком долгих заправок гораздо меньше, чем слишком коротких. На большинстве станций длительных заездов меньше 5%. Простимся с ними без печали.
Сохраняем результаты
Подведём итоги исследования.
Бывает, что заправляются долго. Причём повсюду. Только доля таких заправок невелика. В гистограммах заездов на АЗС с рекордно долгими заправками — ничего особенного, выглядят они ожидаемо.
Было решено отбросить слишком долгие заезды (более 1000 секунд), а для надёжности - вместо среднего арифметического времени рассчитывать медианное. Тогда эти заезды не исказят оценку типичной продолжительности заправки.
А вот с короткими заездами всё не так радужно. Их гораздо больше. Есть АЗС, где коротких заправок большинство, а то и все 100%. Что-то здесь не то.
Ошибка в исходных данных? Сформулируйте проблему, чтобы упростить поиск потенциальной ошибки в алгоритме выгрузки данных. Правильное сообщение об ошибке, или баг-репорт (от англ. bug report — «сообщение об ошибке»), должно чётко объяснять, в чём именно ошибка и как её найти.
Какое сообщение об ошибке позволит коллегам понять проблему и попытаться устранить её:
Данные вообще неадекватны, по ним нельзя посчитать нормально то, что нам нужно. Пользоваться этим невозможно. Проверьте, что вы там выгрузили.
В данных проблема: слишком много быстрых проездов через АЗС.
Правильный ответ Вот пример АЗС, где длительность большинства заездов меньше 60 секунд. Эти данные кажутся неправдоподобными. Такие быстрые заезды есть почти везде.
Когда была построена гистограмма распределения продолжительности заездов, было обнаружено, что форма распределения вовсе не похожа на ожидаемое распределение Пуассона, а имеет аномальный пик быстрых заездов около нуля.
Вы часами исследовали данные и хорошо понимаете суть проблемы. Коллеги, отвечающие за выгрузку, ничего этого не знают. Поэтому нужно чётко формулировать, где вы видите проблему. Для этого не подходит описание «слишком быстрые» или «пик около 0». Нужно быть точнее.
Вы видели гистограмму и обнаружили, что проблемные заезды — те, что короче 60 секунд. Это и нужно сказать.
Следует сообщить, что вы видите проблему в том, что таких заправок слишком много (если бы такие заезды составляли 1% от всех данных, вы бы не сочиняли баг-репорт).
Облегчим работу коллег, сфокусируем их внимание на самой проблемной АЗС — там будет легче отловить ошибку. Или понять, что это не ошибка, а какое-то пока неведомое явление.
Найдём эту проблемную АЗС. Напомним, что в прошлом уроке вы уже считали долю аномально быстрых заправок для каждой АЗС:
print(too_fast_stat.sort_values('too_fast', ascending=False).head())
too_fast
id
c96c61cd 1.000000
c527c306 1.000000
5372547e 1.000000
792b6ded 0.996253
bd1d0bb0 0.982044
АЗС, где 100% заездов аномально быстрые, оказалось целых три! Наверное, их id
нужно передать коллегам, чтобы они искали ошибку. Но сперва посмотрите сами. Методом describe()
можно оценить данные первой АЗС — id == "c96c61cd"
:
data.query('id == "c96c61cd"').describe()
time_spent
count 1.0
mean 5.0
std NaN
min 5.0
25% 5.0
50% 5.0
75% 5.0
max 5.0
Всего один заезд. Это неудачный баг-репорт, ведь АЗС с одним заездом странная сама по себе. Если на ней и была какая-то проблема, не факт, что она массовая.
Для баг-репорта нужна «полноценная» АЗС с достаточным числом аномально быстрых заездов. После аналогичной проверки следующих id
в списке было обнаружено, что нормальная статистика есть только по АЗС 792b6ded
:
data.query('id == "792b6ded"').describe()
time_spent
count 4270.000000
mean 5.448712
std 8.597126
min 0.000000
25% 2.000000
50% 3.000000
75% 5.000000
max 228.000000
Подходит ли эта АЗС для сообщения об ошибке?
Нет, распределение не похоже на нормальное — минимум слишком близок к 25%-й квартили.
Нет, максимальное значение больше 60 секунд, а нам нужны примеры с заездами менее 60 секунд.
Нет, минимальное время проезда - 0 секунд: такого вообще не бывает, должно быть хотя бы 0.01 секунды, но не 0.
Правильный ответ
Да, подходит. 4270 заправок — это активная предпринимательская деятельность.
Больше 4000 заездов, но почти все очень-очень короткие. 75% укладывается в 5 секунд. Первый квартиль составляет 2 секунды. Это значит, что как минимум 25% укладывается в 2 секунды.
Это очень удачный пример для баг-репорта. Можно передать как сам id, так и статистику по этой АЗС.
Заключение
Ура! Вы узнали, что такое срезы данных и как их строить. И да, ещё внимательнее посмотрели на АЗС: выявили зависимость продолжительности заправки от времени заезда и нашли долю аномально быстрых и долгих заправок на разных станциях. Это было непросто.
Как вы это сделали:
- Применили инструменты из первой темы:
hist()
,boxplot()
,describe()
— и посмотрели на распределение. - Построили графики методом
plot()
. - Научились помечать выборку в общем объёме данных (добавив столбец
too_fast
) и смотреть, как распределён срез в совокупности данных. - Узнали, что такое баг-репорт и как правильно формулировать проблему, чтобы упростить поиск ошибки в алгоритме выгрузки.
Теперь коллеги точно не отвертятся и обязательно поправят данные или выгрузят новые :-)
Заберите с собой
Чтобы ничего не забыть, скачайте шпаргалку и конспект темы.
Где ещё почитать про срезы данных
Индексы и срезы по индексам
Индексация с использованием логических выражений
Проверочные задания. Изучение срезов данных
Чтобы пройти тест нужно правильно ответить на 5 вопросов из 10.
Время на прохождение: 20 минут
Задание 1 из 10
Как можно получить срез данных в pandas? Выберите все подходящие способы.
Правильный ответ
Использовать список из True и False в качестве индекса
Правильный ответ
Использовать значения, которые возвращает метод isin(), в качестве индекса
Применить метод pivot_table()
Правильный ответ
Применить метод query()
Бывает, что для анализа нужны не все данные, а только те, что соответствуют какому-то условию. Тогда аналитик работает со срезом данных. Чтобы получить срез, нужно задать условие, например, можно сравнить значения с помощью операторов > или <. Такое условие поможет получить булев массив из True и False, который передают в качестве индекса датафрейма. Условием может быть проверка на наличие конкретных значений, тогда пригодится метод isin(). Другой способ — передать условие методу query() и применить его к датафрейму.
Задание 2 из 10
Какой код выведет список из True и False?
df['column_a' > 10]
df[df['column_a'] < 10]
df[df['column_a' != 10]]
Правильный ответ
df['column_a'] == 10
Если сравнить значения в колонке column_a с числом 10, на выходе получится объект Series из True и False. А чтобы получить срез, его передают в качестве индекса датафрейма: df[df['column_a'] == 10].
Задание 3 из 10
Выберите синтаксически правильный запрос с методом query().
Правильный ответ
df.query('height == 120')
df.pd.query('height == 120')
df.query('height = 120')
df.query(height = 120)
Метод query() вызывают для датафрейма, а не для библиотеки pandas. Обратите внимание на синтаксис: название колонки не заключают в кавычки, только всё условие целиком.
Задание 4 из 10
Выберите все варианты кода, которые оставят только три последние строчки датафрейма.
color number
0 белый 4
1 синий 5
2 синий 3
3 белый 2
4 белый 1
Правильный ответ
df[~df['number'].isin([4,5])]
Правильный ответ
df[df['number'].isin([1,2,3])]
df[df['number']].isin([1,2,3])
df['number'].~isin([4,5])
Метод isin() применяют к колонке и затем передают результат в качестве индекса датафрейма. Оператор ~ меняет значение на противоположное: в срез, который создаст код df[~df['number'].isin([4,5])], тоже войдут строки со значениями 1, 2, 3 в столбце number.
Задание 5 из 10
Выберите все варианты кода, которые оставят последние две строчки датафрейма?
color number
0 белый 4
1 синий 5
2 синий 3
3 белый 2
4 белый 1
Правильный ответ
df.query('color == "белый" and number < 3')
Правильный ответ
df.query('color != "синий" and number < 3')
df.query('color = "белый" and number < 3')
df.query(color != "синий" and number < 3)
В аргументе метода query() можно задавать несколько условий. Не забудьте про синтаксис: равенство проверяют с помощью ==, а элемент для сравнения заключают в двойные кавычки.
Задание 6 из 10
Выберите код, который включит в метод query() внешнюю переменную.
Правильный ответ
df.query('color == @variable')
df.query('color == $variable')
df.query('color == "variable"')
df.query('color == %variable')
Чтобы не создавать громоздкое условие, нужное значение сохраняют в отдельную переменную, а затем добавляют его в аргумент метода query().
Задание 7 из 10
Выберите корректный код для выборки, соответствующей нескольким условиям.
df[(df['A'] > 0) AND (df['B'] > 10)]
df[(df['A'] > 0) and (df['B'] > 10)]
Правильный ответ
df[(df['A'] > 0) & (df['B'] > 10)]
df[df['A'] > 0 & df['B'] > 10]
В аргументе метода query() можно объединять условия с помощью and и or. В другом случае условия объединяют операторами & или |. Обратите внимание, что условия нужно заключить в скобки.
Задание 8 из 10
Выберите код, который изменит тип значений в столбце date на datetime в формате '11-03-07 17:15'. Такая запись соответствует 7 марта 2011 года.
Правильный ответ
pd.to_datetime(df['date'], format='%y-%m-%d %H:%M')
pd.to_datetime(df['date'], format='%y-%m-%dT%H:%M')
pd.to_datetime(df['date'], format='%y %m %d %h %m')
pd.to_datetime(df['date'], format='%Y %M %D %H %M')
Чтобы перевести строки в даты, нужно верно передать формат в аргументе метода to_datetime(). Порядок чисел и разделяющие символы тоже нужно сохранить.
Задание 9 из 10
Почему можно заподозрить выбросы на таком графике?
image
Большое количество значений рядом с нулём.
Много значений около медианы.
На графике видно несколько пиков значений.
Правильный ответ
Несколько значений отделены от основной массы.
Работая с данными АЗС, вы столкнулись с большим количеством значений около нуля. Таких значений было много, поэтому их было сложно считать выбросами. Пиковые значения около медианы нельзя назвать маловероятными, например, так выглядит нормальное распределение значений. А вот значения, которые сильно отличаются от других элементов, можно посчитать выбросами. Но это не значит, что от выбросов нужно сразу же избавиться.
Задание 10 из 10
Выберите код, который построит такой график.
image
Правильный ответ
df.plot(x='b', y='a', style='o-', grid=True)
df.plot(x='b', y='a', style='o', grid=False)
df.hist(x='b', y='a', style='x', grid=True)
df.line(x='b', y='a', style='dot', grid=False)
Простой график по точкам можно нарисовать в pandas методом plot(). Параметры метода управляют внешним видом графика. Аргументу style передают форму точек, а аргумент grid=True добавит сетку.
Следующая тема: ИАД. Работа с несколькими источниками данных
Вернуться в раздел: Исследовательский анализ данных
Вернуться в оглавление: Я.Практикум