Следующая тема: ПД. Системное и критическое мышление в работе аналитика

Вернуться к разделу: "Предобработка данных"

Вернуться в оглавление: Я.Практикум

1.Введение

В огромном датафрейме вам может понадобиться определённый сегмент. Допустим, интересуют данные по узкой возрастной категории или одному городу. Чтобы выделить их, прибегают к категоризации — объединению избранных данных в произвольные группы по заданному критерию. Аналитики пишут функции на Python, чтобы выделить такие группы. Вспомним, как это делать, и применим к реальному датасету.

Чему вы научитесь

  • Выделять из данных словарь категорий;
  • Разделять данные на категории по числовому признаку;
  • Писать функции для обработки сразу нескольких ячеек в строке.

Сколько времени это займет:

4 урока от 10 до 25 минут

Постановка задачи:

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

2.Знакомство с данными

Важно уметь не только привлекать клиентов, но и удерживать их.

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

Вам передали статистику обращений в службу поддержки Яндекс.Маркета: id каждого пользователя с темой и временем обращения.

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

Задача №1

Прочитайте файл с отзывами, размещённый по адресу: /datasets/support.csv

Сохраните таблицу в переменной support. Выведите первые 10 строк.

Путь к файлу: /datasets/support.csv

import pandas as pd support = pd.read_csv('/datasets/support.csv') print (support.head(10))

    user_id                Тип обращения  type_id      Время обращения
0  DNcd8dnS   Жалоба на товар в магазине        3  2019-03-28 13:58:24
1  0e9MvwGs                Мошенничество        5  2019-03-08 17:11:59
2  boyDUG4C                Мошенничество        5  2019-03-03 17:52:34
3  5jMA27s1   Жалоба на товар в магазине        3  2019-03-16 15:18:21
4  wvtyctOK             Накрутка отзывов        2  2019-03-13 14:43:14
5  zvN8W1tc         Жалоба на видеообзор        8  2019-03-29 20:32:17
6  bBmybSbr             Не работает сайт        1  2019-04-08 21:37:15
7  5JX1P5G8  Продажа запрещенных товаров        6  2019-04-06 12:15:20
8  pbbG3xWx             Не работает сайт        1  2019-04-09 17:30:43
9  vZWtTovT   Жалоба на товар в магазине        3  2019-04-16 12:26:16


Всё не то и всё не так, когда изучаешь список обращений в службу поддержки.

Задача №2

Просмотрите общую информацию о наборе данных.

import pandas as pd
support = pd.read_csv('/datasets/support.csv')
print (support.info())

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3000 entries, 0 to 2999
Data columns (total 4 columns):
 #   Column           Non-Null Count  Dtype
---  ------           --------------  -----
 0   user_id          3000 non-null   object
 1   Тип обращения    3000 non-null   object
 2   type_id          3000 non-null   int64
 3   Время обращения  3000 non-null   object
dtypes: int64(1), object(3)
memory usage: 93.9+ KB
None

Ладно хоть с типами данных всё хорошо, а вот имена столбцов записаны некорректно. Разберитесь с этим.

Задача №3

Замените названия столбцов методом rename():

  • Тип обращения поменяйте на type_message;
  • Время обращения — на timestamp.

Столбцы user_id и type_id оставьте без изменений.

Проверьте успешность замены методом info().

import pandas as pd
support = pd.read_csv('/datasets/support.csv')
support = support.rename(columns={'Тип обращения':'type_message'})
support = support.rename(columns={'Время обращения':'timestamp'})
print (support.info())

Результат
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3000 entries, 0 to 2999
Data columns (total 4 columns):
 #   Column        Non-Null Count  Dtype
---  ------        --------------  -----
 0   user_id       3000 non-null   object
 1   type_message  3000 non-null   object
 2   type_id       3000 non-null   int64
 3   timestamp     3000 non-null   object
dtypes: int64(1), object(3)
memory usage: 93.9+ KB
None

Столбец с именем на type и столбец 'timestamp' отныне ваши постоянные спутники. Вы с ними свыкнетесь так, что вам будет их не хватать даже в отпуске.

3.Декомпозиция таблиц

Типы обращений в поддержку хранятся в виде строк разной длины: 
 
0  DNcd8dnS   Жалоба на товар в магазине        3  2019-03-28 13:58:24
1  0e9MvwGs                Мошенничество        5  2019-03-08 17:11:59
2  boyDUG4C                Мошенничество        5  2019-03-03 17:52:34
3  5jMA27s1   Жалоба на товар в магазине        3  2019-03-16 15:18:21
4  wvtyctOK             Накрутка отзывов        2  2019-03-13 14:43:14
5  zvN8W1tc         Жалоба на видеообзор        8  2019-03-29 20:32:17
6  bBmybSbr             Не работает сайт        1  2019-04-08 21:37:15
7  5JX1P5G8  Продажа запрещенных товаров        6  2019-04-06 12:15:20
8  pbbG3xWx             Не работает сайт        1  2019-04-09 17:30:43
9  vZWtTovT   Жалоба на товар в магазине        3  2019-04-16 12:26:16

К чему приводит такой способ хранения?

  • Усложняется визуальная работа с таблицей.
  • Увеличивается размер файла и время обработки данных.
  • Чтобы отфильтровать данные по типу обращения, приходится набирать его полное название. А в нём можно ошибиться.
  • Создание новых категорий и изменение старых отнимает много времени. Предупредить появление этих проблем можно. Создадим отдельный «справочник», где названию категории будет соответствовать номер. И в будущих таблицах будем обращаться уже не к длинной строке, а к её числовому обозначению.

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

import pandas as pd
data = pd.read_csv('food_market_stats.csv')
print(data.head(10))
id          cuisine     rating                name
0   1  Ближневосточная     5.0  The Grand Barbecue
1   1  Ближневосточная     5.0  The Grand Barbecue
2   1  Ближневосточная     5.0  The Grand Barbecue
3   1  Ближневосточная     4.0  The Grand Barbecue
4   1  Ближневосточная     4.0  The Grand Barbecue
5   1  Ближневосточная     5.0  The Grand Barbecue
6   1  Ближневосточная     5.0  The Grand Barbecue
7   1  Ближневосточная     3.0  The Grand Barbecue
8   1  Ближневосточная     1.0  The Grand Barbecue
9   1  Ближневосточная     5.0  The Grand Barbecue

В таблице уникальный идентификатор id, кухня ресторана cuisine, его оценка от посетителя rating и название name. Задача — найти средний рейтинг для каждого ресторана.

Уточним, какие виды кухонь подают в ресторанах:

print(data['cuisine'].value_counts().head(15))
Кавказская                             3289
Средиземноморская                      2678
Японская                               2670
Русская                                2521
Ближневосточная                        2343
Азиатская                              1977
Русская|Азиатская                      1695
Кавказская|Русская                     1586
Средиземноморская|Русская|Японская     1473
Ближневосточная|Японская               1137
Русская|Кавказская                     1111
Японская|Азиатская                     1089
Русская|Японская|Кавказская             984
Ближневосточная|Японская|Кавказская     886
Кавказская|Азиатская                    799
Name: kitchen, dtype: int64

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

Узнаем, сколько ресторанов всего:

print(len(data['id'].unique()))

>> 273

Получается, что и название, и тип кухни многократно повторяются в данных, а строки с ними занимают много места. Создадим “справочник” и оптимизируем данные.

Разделим таблицу data на две части:

В первой оставим столбцы id и rating (оценки, поставленные ресторану посетителями).

rest_log = data[['id','rating']]
print(rest_log.head(10))
id  rating
0   1     5.0
1   1     5.0
2   1     5.0
3   1     4.0
4   1     4.0
5   1     5.0
6   1     5.0
7   1     3.0
8   1     1.0
9   1     5.0

Вторая станет «справочником»: каждому из 273 ресторанов запишем id, название name и тип кухни cuisine.

rest_dict = data[['id', 'cuisine', 'name']]
print(rest_dict.head(10))
id          cuisine                name
0   1  Ближневосточная  The Grand Barbecue
1   1  Ближневосточная  The Grand Barbecue
2   1  Ближневосточная  The Grand Barbecue
3   1  Ближневосточная  The Grand Barbecue
4   1  Ближневосточная  The Grand Barbecue
5   1  Ближневосточная  The Grand Barbecue
6   1  Ближневосточная  The Grand Barbecue
7   1  Ближневосточная  The Grand Barbecue
8   1  Ближневосточная  The Grand Barbecue
9   1  Ближневосточная  The Grand Barbecue
В «справочнике» вы заметили большое количество дубликатов. Их нужно удалить. Применим знакомую цепочку методов: drop_duplicates() и reset_index().
rest_dict = rest_dict.drop_duplicates().reset_index(drop=True)
print(rest_dict.head())
id                             cuisine                  name
0   1                     Ближневосточная    The Grand Barbecue
1   2                            Японская       The Bitter Bite
2   3                          Кавказская       The Curry Apple
3   4                            Японская  The Caviar Courtyard
4   5  Кавказская|Ближневосточная|Русская   The Little Brothers
После удаления дубликатов представление данных с оценками посетителей стало компактнее. Таблицу легко группировать по id ресторана и находить средний рейтинг.
print(rest_log.groupby('id').mean().sort_values('rating',ascending=False).head(10))

rating
id           
134  4.159420
111  4.133333
167  4.131250
113  4.113208
162  4.099010
219  4.095238
20   4.090909
264  4.031746
47   4.029167
77   4.024490

Надо сказать, что на практике декомпозировать таблицы приходится не так часто, чаще требуется их объединить по какому-то столбцу.

Вы научитесь это делать в следующем спринте. Однако сам навык создания подобных таблиц-«справочников» очень полезен.

Проделаем то же самое с обращениями в службу поддержки.

Задача №1

Уточните, сколько раз встречается каждый тип обращений. Результат выведите на экран.

import pandas as pd

support = pd.read_csv('/datasets/support_upd.csv')
print (support['type_message'].value_counts().head(10))
Жалоба на товар в магазине      606
Мошенничество                   586
Продажа поддельной продукции    312
Не работает сайт                311
Продажа запрещенных товаров     303
Накрутка отзывов                302
Жалоба на видеообзор            297
Отзыв удалён                    283
Name: type_message, dtype: int64

И такая дребедень целый день.

Задача №2

Создайте новую таблицу support_log. Из таблицы support перенесите следующие столбцы: 'user_id', 'type_id', 'timestamp'. Первые 10 строк support_log напечатайте на экране.

import pandas as pd

support = pd.read_csv('/datasets/support_upd.csv')
support_log = support[['user_id','type_id','timestamp']]
print (support_log.head(10))
Результат
    user_id  type_id            timestamp
0  DNcd8dnS        3  2019-03-28 13:58:24
1  0e9MvwGs        5  2019-03-08 17:11:59
2  boyDUG4C        5  2019-03-03 17:52:34
3  5jMA27s1        3  2019-03-16 15:18:21
4  wvtyctOK        2  2019-03-13 14:43:14
5  zvN8W1tc        8  2019-03-29 20:32:17
6  bBmybSbr        1  2019-04-08 21:37:15
7  5JX1P5G8        6  2019-04-06 12:15:20
8  pbbG3xWx        1  2019-04-09 17:30:43
9  vZWtTovT        3  2019-04-16 12:26:16

Теперь неизвестно кто жалуется непонятно на что. Зато знаем точно, когда.
 
Задача №3
 
Создайте новую таблицу support_dict. Из таблицы support перенесите следующие столбцы: 'type_message', 'type_id'. Первые 10 строк support_dict выведите на экран.
import pandas as pd

support = pd.read_csv('/datasets/support_upd.csv')
support_dict = support[['type_message', 'type_id']]
print (support_dict.head(10))
Результат
                  type_message  type_id
0   Жалоба на товар в магазине        3
1                Мошенничество        5
2                Мошенничество        5
3   Жалоба на товар в магазине        3
4             Накрутка отзывов        2
5         Жалоба на видеообзор        8
6             Не работает сайт        1
7  Продажа запрещенных товаров        6
8             Не работает сайт        1
9   Жалоба на товар в магазине        3

Вы создали таблицу, где каждому типу жалобы сопоставлен идентификатор.
Задача №4
 
Удалите дубликаты в таблице support_dict и распечатайте её в порядке возрастания значений столбца type_id.
import pandas as pd

support = pd.read_csv('/datasets/support_upd.csv')
support_dict=support[['type_message','type_id']]
support_dict = support_dict.drop_duplicates().reset_index(drop=True)
print (support_dict.sort_values('type_id',ascending=True))
Результат
                   type_message  type_id
4              Не работает сайт        1
2              Накрутка отзывов        2
0    Жалоба на товар в магазине        3
6  Продажа поддельной продукции        4
1                 Мошенничество        5
5   Продажа запрещенных товаров        6
7                  Отзыв удалён        7
3          Жалоба на видеообзор        8

Грустит самурай Отзывы накрутили

Поддержка, как быть?

4.Категоризация по числовым диапазонам

Вам как аналитику задали вопрос: клиентов какого возраста больше всего в нашей базе? Ознакомимся с данными:
 
import pandas as pd

clients = pd.read_csv('stats_by_age.csv')
print(clients.head())
\ id first_name last_name age
0 1 Alikee O'Mullally 24
1 2 Rosella Winnard 29
2 3 Peri Talmadge 37
3 4 Brose Attwooll 21
4 5 Rycca Caunter 29
 
В столбце FIRST_NAME значатся имена клиентов, в столбце LAST_NAME — их фамилии, а возраст — целые числа в столбце AGE. Количество клиентов каждого возраста можно посчитать методом value_counts():
print(clients['age'].value_counts())
17    13
20    10
37     9
....
72     1
73     1
16     1
Name: age, dtype: int64

Какие выводы можно сделать из этих данных?

Неужели компании необходимо сосредоточиться на двадцати-, семнадцати- и тридцатисемилетних, а всех, кому 16 или 38, оставить без внимания?

И как тогда быть на следующий год?

Работать с единичными отрывками и делать из них статистические выводы нельзя. Значит, нужно сгруппировать данные, чтобы численности каждой группы хватало для формулировки выводов.

Нужна категоризация — объединение данных в категории. Распределим клиентов так:

  • Клиенты до 18 лет включительно попадают в категорию «дети»;
  • Клиенты от 19 до 64 лет — категория «взрослые»;
  • Клиенты 65 лет и старше принадлежат к категории «пенсионеры».

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

На вход функции попадает возраст, а возвращает она категорию клиента. Опишем функцию:

def age_group(age):
    """
    Возвращает возрастную группу по значению возраста age, используя правила:
    - 'дети', если age <= 18 лет;
    - 'взрослые', если age от 19 до 64;
    - 'пенсионеры' — от 65 и старше.
    """
    
    if age <= 18:
        return 'дети'
    if age <= 64:
        return 'взрослые'
    return 'пенсионеры'
Протестируем работу функции для каждого правила. Проверим, в какую категорию попадёт четырнадцатилетний клиент:
print(age_group(14))
дети
... пятидесятипятилетний: 
print(age_group(55))
взрослые
... и наконец, семидесятилетний: 
print(age_group(70))
пенсионеры

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

Для этого нужен метод apply(): он берёт значения столбца датафрейма и применяет к ним функцию из своего аргумента. Здесь apply() следует вызвать для столбца AGE, так как в нём содержатся данные, которые функция примет на вход. Аргументом метода станет сама функция age_group.

clients['age_group'] = clients['age'].apply(age_group)
print(clients.head(10))
	id	first_name	last_name	age	age_group
0	1	Alikee	O'Mullally	24	взрослые
1	2	Rosella	Winnard	29	взрослые
2	3	Peri	Talmadge	37	взрослые
3	4	Brose	Attwooll	21	взрослые
4	5	Rycca	Caunter	29	взрослые
5	6	Jennine	Gamage	27	взрослые
6	7	Archibold	Wife	72	пенсионеры
7	8	Karalynn	Busk	23	взрослые
8	9	Kari	Canlin	29	взрослые
9	10	Asia	Cudde	28	взрослые
Выведем статистику по возрастной группе методом value_counts():
print(clients['age_group'].value_counts())

взрослые      145
дети           22
пенсионеры      7
Name: age_group, dtype: int64

В таком виде данные годятся для анализа.

Также методом apply() можно создать столбец на основе данных из нескольких других столбцов.

Допустим, нужно сделать новый столбец с полным именем клиента. Напишем функцию, которая принимает строку датафрейма как аргумент row. Строка таблицы — это объект Series, поэтому в теле функции можно обратиться к отдельным её ячейкам:

def make_full_name(row):
    """
    Возвращает полное имя (сочетание имени и фамилии)
    """
    full_name = row['first_name'] + ' ' + row['last_name']
    return full_name
Когда метод apply() оперирует данными из нескольких столбцов, его вызывают ко всему датафрейму. В таком случае он принимает не только название функции, но и параметр axis: со значением 1, чтобы применить метод ко всем строкам датафрейма, и со значением 0 — ко всем столбцам.
clients['full_name'] = clients.apply(make_full_name, axis=1)
print(clients.head(10))
   id first_name   last_name  age          full_name
0   1     Alikee  O'Mullally   24  Alikee O'Mullally
1   2    Rosella     Winnard   29    Rosella Winnard
2   3       Peri    Talmadge   37      Peri Talmadge
3   4      Brose    Attwooll   21     Brose Attwooll
4   5      Rycca     Caunter   29      Rycca Caunter
5   6    Jennine      Gamage   27     Jennine Gamage
6   7  Archibold        Wife   72     Archibold Wife
7   8   Karalynn        Busk   23      Karalynn Busk
8   9       Kari      Canlin   29        Kari Canlin
9  10       Asia       Cudde   28         Asia Cudde
Метод apply() позволяет применить функцию к каждой строке датафрейма без цикла. В pandas перебор строк циклами — это неоптимальный путь, «сжирающий» время и память.
 
Задача №1
 
Сгруппируйте данные по типу события и посчитайте количество событий. Сохраните группировку с подсчётом в переменной support_log_grouped и выведите её на экран.
import pandas as pd

support_log = pd.read_csv('/datasets/support_log.csv')
support_log_grouped = support_log.groupby('type_id').count()
print (support_log_grouped)
Результат
         Unnamed: 0  user_id  timestamp
type_id
1               311      311        311
2               302      302        302
3               606      606        606
4               312      312        312
5               586      586        586
6               303      303        303
7               283      283        283
8               297      297        297

Ваша наука — борьба с самыми разными проблемами. Любая наука начинается с классификации. Вот противники разделены на типы и пересчитаны по головам.
Задача №2
 
Напишите функцию alert_group(messages), которая оценивает приоритет в зависимости от количества сообщений. Если параметр не более 300, она должна возвращать строку 'средний', если значение параметра от 301 до 500 включительно, тогда строку 'высокий'. Для более высоких значений должна возвращаться строка 'критичный'. Проверьте, что ваша функция отвечает верно, когда ей передают числа 104501000. Каждое значение выводите на новой строке.
import pandas as pd

support_log = pd.read_csv('/datasets/support_log.csv')
support_log_grouped = support_log.groupby('type_id').count()

def alert_group(messages):
    if messages <= 300:
        return 'средний'
    elif messages <= 500:
        return 'высокий'
    return 'критичный'

print (alert_group(10))
print (alert_group(450))
print (alert_group(1000))

средний
высокий
критичный

Трудности есть большие, очень большие и средние. А мелких нет. Потому что каждое обращение клиента важно. Привыкайте!

Задача №3

Добавьте к таблице support_log_grouped столбец 'alert_group', где хранятся результаты применения вашей функции alert_group().

Закомментируйте результаты предыдущего задания. Посмотрите верхние 10 строк support_log_grouped: убедитесь, что функция правильно расставила приоритеты.

import pandas as pd

support_log = pd.read_csv('/datasets/support_log.csv')
support_log_grouped = support_log.groupby('type_id').count()

def alert_group(messages):
    if messages <= 300:
        return 'средний'
    elif messages <= 500:
        return 'высокий'
    return 'критичный'

#print (alert_group(10))
#print (alert_group(450))
#print (alert_group(1000))

support_log_grouped['alert_group'] = support_log_grouped['user_id'].apply(alert_group)
print (support_log_grouped.head(10))
Результат
         Unnamed: 0  user_id  timestamp alert_group
type_id
1               311      311        311     высокий
2               302      302        302     высокий
3               606      606        606   критичный
4               312      312        312     высокий
5               586      586        586   критичный
6               303      303        303     высокий
7               283      283        283     средний
8               297      297        297     средний

Каждому обращению прописан приоритет. Если критичных нет, можете устроить совещание. А если есть, придётся работать.
 
Задача №4
 
Посчитайте количество обращений по каждому приоритету и выведите результат на экран. Вывод от прошлых задач при необходимости закомментируйте.
import pandas as pd

support_log = pd.read_csv('/datasets/support_log.csv')
support_log_grouped = support_log.groupby('type_id').count()

def alert_group(messages):
    if messages <= 300:
        return 'средний'
    elif messages <= 500:
        return 'высокий'
    return 'критичный'

#print (alert_group(10))
#print (alert_group(450))
#print (alert_group(1000))

support_log_grouped['alert_group'] = support_log_grouped['user_id'].apply(alert_group)
#print (support_log_grouped.head(10))

print (support_log_grouped.groupby('alert_group').sum())
 
Результат
             Unnamed: 0  user_id  timestamp
alert_group
высокий            1228     1228       1228
критичный          1192     1192       1192
средний             580      580        580

Спросите у Алисы, как грамотно сообщать своим сотрудникам, что всё плохо.
 

5.Функция для одной строки

Вашу изначальную задачу усложнили: теперь необходимо поделить «взрослых» на занятых и безработных. Разработчики добавили новый столбец ['unemployed'], где значение 1 означает, что у клиента нет работы, а значение 0 — что работа есть. Взглянем на обновленные данные:
 
import pandas as pd
clients = pd.read_csv('/datasets/stats_by_age_employment.csv')
clients.head(10)
	id	first_name	last_name	age	unemployed
    ---------------------------------------------
0	1	Alikee	O'Mullally	24	1
1	2	Rosella	Winnard	29	0
2	3	Peri	Talmadge	37	0
3	4	Brose	Attwooll	21	1
4	5	Rycca	Caunter	29	0
5	6	Jennine	Gamage	27	0
6	7	Archibold	Wife	72	1
7	8	Karalynn	Busk	23	0
8	9	Kari	Canlin	29	0
9	10	Asia	Cudde	28	0

В прошлом уроке вы научились писать функции, работающие лишь с одним значением на входе. Так, функции age_group, возвращающей возрастную категорию клиента, требовалось значение столбца ['age'].

Однако правила распределения клиентов по категориям перестали зависеть только от возраста:

  • Клиенты до 18 лет включительно по-прежнему в категории «дети»;
  • Клиенты от 19 до 64 лет при наличии работы — категория «занятые»;
  • Клиенты от 19 до 64 лет без работы — «безработные»;
  • Клиенты старше 65 лет остаются в категории «пенсионеры».

Получается, называя клиента «занятым» или «безработным», принимают во внимание значения двух столбцов: возраста ['age'] и занятости ['unemployed'].

Чтобы функция учитывала несколько столбцов датафрейма, в качестве аргумента ей передают всю строку целиком. Обозначим строку переменной row, а в коде функции обратимся к конкретным значениям столбцов row['age'] и row['unemployed'].

import pandas as pd
clients = pd.read_csv('/datasets/stats_by_age_employment.csv')

def age_group_unemployed(row):
"""
Возвращает возрастную группу по значению возраста age и занятости unemployed, используя правила:
- 'дети' при значении age <= 18 лет
- 'безработные' при значении age от 19 до 64 лет включительно и значении unemployed = 1
- 'занятые' при значении age от 19 до 64 лет включительно и значении unemployed = 0
- 'пенсионеры' во всех остальных случаях
"""
    age = row['age']
    unemployed = row['unemployed']

    if age <= 18:
        return 'дети'

    if age <= 64:
        if unemployed == 1:
            return 'безработные'

        return 'занятые'

    return 'пенсионеры'

В прошлом уроке, тестируя работу функции, мы меняли количество лет в её аргументе. Сейчас на входе не только возраст, но и занятость, значит, для проверки нужно передавать целую строку датафрейма с этими значениями. Это делают в несколько шагов:

  1. Создают два списка. В одном — значения, в другом — названия столбцов датафрейма
    row_values = [24, 1] #значения возраста и занятости 
    row_columns = ['age', 'unemployed'] #названия столбцов
  2. Формируют строку:
    row = pd.Series(data=row_values, index=row_columns) 
  3. Передают строку в качестве аргумента функции для тестирования:
    age_group_unemployed(row)

Проверим работу функции при разных значениях на входе:

...
row_values = [24, 1] #клиенту 24 года, и он безработный
row_columns = ['age', 'unemployed']
row = pd.Series(data=row_values, index=row_columns) 
age_group_unemployed(row)
'безработные' 
...
row_values = [36, 0] #клиенту 36 лет, и у него есть работа
row_columns = ['age', 'unemployed']
row = pd.Series(data=row_values, index=row_columns) 
age_group_unemployed(row)
'занятые'
 

Функция работает корректно. Осталось создать новый столбец clients['age_group'] со значениями-результатами работы функции age_group_unemployed(). Как и в прошлом уроке, вызовем метод apply(), однако с двумя важными отличиями от прошлого примера:

  1. Метод apply() применяем не к столбцу clients['age'], а к датафрейму clients.
  2. По умолчанию Pandas передаёт в функцию age_group_unemployed() столбец. Чтобы на вход в функцию отправлялись строки, нужно указать параметр axis = 1 метода apply().

С учётом этих двух замечаний новый столбец age_group формируется так:

clients['age_group'] = clients.apply(age_group_unemployed, axis=1)
Проверим результат работы функции:
import pandas as pd
clients = pd.read_csv('/datasets/stats_by_age_employment.csv')
def age_group_unemployed(row):
"""
Возвращает возврастную группу по значению возраста age и занятости unemployed, используя правила:
- 'дети' при значении age <= 18 лет
- 'безработные' при значении age от 19 до 64 лет включительно и значении unemployed = 1
- 'занятые' при значении age от 19 до 64 лет включительно и значении unemployed = 0
- 'пенсионеры' во всех остальных случаях
"""
    age = row['age']
    unemployed = row['unemployed']

    if age <= 18:
        return 'дети'

    if age <= 64:
        if unemployed == 1:
            return 'безработные'

        return 'занятые'

    return 'пенсионеры'

clients['age_group'] = clients.apply(age_group_unemployed, axis=1)
print(clients.head(10))
	id	first_name	last_name	age	unemployed	age_group
0	1	Alikee	O'Mullally	24	1	безработные
1	2	Rosella	Winnard	29	0	занятые
2	3	Peri	Talmadge	37	0	занятые
3	4	Brose	Attwooll	21	1	безработные
4	5	Rycca	Caunter	29	0	занятые
5	6	Jennine	Gamage	27	0	занятые
6	7	Archibold	Wife	72	1	пенсионеры
7	8	Karalynn	Busk	23	0	занятые
8	9	Kari	Canlin	29	0	занятые
9	10	Asia	Cudde	28	0	занятые
 
Функция работает корректно. Применим метод value_counts() для подсчёта значений каждой категории.
print(clients['age_group'].value_counts())
занятые        108
безработные     37
дети            22
пенсионеры       7
Name: age_group, dtype: int64
 
Научившись прописывать функции для одной строки и поделив взрослых на безработных и занятых, вы подготовили более точный отчёт для руководителя.
 
Задача №1
 
Прочитайте файл с отзывами, размещённый по адресу: /datasets/support_log_grouped.csv Результат выведите на экран.
import pandas as pd
#напишите ваш код здесь
support_log_grouped = pd.read_csv('/datasets/support_log_grouped.csv')
print (support_log_grouped)
Результат
   type_id  user_id  timestamp alert_group  importance
0        1      311        311     высокий           1
1        2      302        302     высокий           0
2        3      606        606   критичный           0
3        4      312        312     высокий           1
4        5      586        586   критичный           1
5        6      303        303     высокий           1
6        7      283        283     средний           1
7        8      297        297     средний           0

Лайфхак: делайте срочное и важное. А несрочное и неважное не делайте.
Задача №2
 

Напишите функцию alert_group_importance(row), работающую в соответствии со следующей логикой:

  • Если приоритет 'alert_group' средний, а важность 'importance' оценена в единицу, возвращать команду для отдела поддержки: 'обратить внимание';
  • Если приоритет высокий, а важность — единица, возвращать: 'высокий риск';
  • Если приоритет критичный, а важность — единица, возвращать: 'блокер';

Во всех остальных случаях выводить: 'в порядке очереди'.

Проверьте работоспособность функции для разных значений. Чтобы тренажёр принял решение, оставьте в переменной row_values значение ['высокий', 1].

Не забудьте передать строке датафрейма названия столбцов.

import pandas as pd
support_log_grouped = pd.read_csv('/datasets/support_log_grouped.csv')

def alert_group_importance(row):
    # реализуйте логику функции
    if row['alert_group'] == 'средний' and row['importance'] == 1:
        return 'обратить внимание'
    elif row['alert_group'] == 'высокий' and row['importance'] == 1:
        return 'высокий риск'
    elif row['alert_group'] == 'критичный' and row['importance'] == 1:
        return 'блокер'
    return 'в порядке очереди'
    
row_values = ['высокий', 1]
row_columns = ['alert_group', 'importance']
row = pd.Series(data=row_values, index=row_columns)
print(alert_group_importance(row))

высокий риск

Да вы ещё и тестировщик или тестировщица!

Задача №3

Создайте новый столбец importance_status и сохраните в нём результаты работы функции alert_group_importance(). Закомментируйте вывод предыдущей задачи и напечатайте обновлённый датасет на экране.

import pandas as pd
support_log_grouped = pd.read_csv('/datasets/support_log_grouped.csv')

def alert_group_importance(row):
    # реализуйте логику функции
    if row['alert_group'] == 'средний' and row['importance'] == 1:
        return 'обратить внимание'
    elif row['alert_group'] == 'высокий' and row['importance'] == 1:
        return 'высокий риск'
    elif row['alert_group'] == 'критичный' and row['importance'] == 1:
        return 'блокер'
    return 'в порядке очереди'
    
row_values = ['высокий', 1]
row_columns = ['alert_group', 'importance']
row = pd.Series(data=row_values, index=row_columns)
#print(alert_group_importance(row))
support_log_grouped['importance_status'] = support_log_grouped.apply(alert_group_importance, axis=1)
print(support_log_grouped)
Результат
   type_id  user_id  timestamp alert_group  importance  importance_status
0        1      311        311     высокий           1       высокий риск
1        2      302        302     высокий           0  в порядке очереди
2        3      606        606   критичный           0  в порядке очереди
3        4      312        312     высокий           1       высокий риск
4        5      586        586   критичный           1             блокер
5        6      303        303     высокий           1       высокий риск
6        7      283        283     средний           1  обратить внимание
7        8      297        297     средний           0  в порядке очереди

Благодаря вашим усилиям, агент поддержки больше не будет задаваться вопросами приоритизации и сможет сконцентрироваться на решении проблем.
Задача №4
 
Посчитайте, сколько раз встречается каждый из статусов в столбце importance_status. Закомментируйте вывод предыдущей задачи и напечатайте результат подсчётов на экране.
import pandas as pd
support_log_grouped = pd.read_csv('/datasets/support_log_grouped.csv')

def alert_group_importance(row):
    # реализуйте логику функции
    if row['alert_group'] == 'средний' and row['importance'] == 1:
        return 'обратить внимание'
    elif row['alert_group'] == 'высокий' and row['importance'] == 1:
        return 'высокий риск'
    elif row['alert_group'] == 'критичный' and row['importance'] == 1:
        return 'блокер'
    return 'в порядке очереди'
    
row_values = ['высокий', 1]
row_columns = ['alert_group', 'importance']
row = pd.Series(data=row_values, index=row_columns)
#print(alert_group_importance(row))
support_log_grouped['importance_status'] = support_log_grouped.apply(alert_group_importance, axis=1)
#print(support_log_grouped)
print (support_log_grouped['importance_status'].value_counts())
Результат
высокий риск         3
в порядке очереди    3
блокер               1
обратить внимание    1
Name: importance_status, dtype: int64

Признание проблемы — половина успеха в её разрешении. А вы не просто признали, ещё классифицировали, посчитали и в список аккуратно положили. Это уже 90% успеха. Осталась самая малость — решить проблему.
 

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

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

В этой теме вы классифицировали обращения в службу поддержки. Теперь легко определить, какие проблемы требуют срочного решения, а какие можно отложить. Команда поддержки может не тратить время на выявление приоритетных задач, а сосредоточиться на качественном обслуживании клиентов. Возможно, ваша классификация вызовет чрезмерный рост количества счастливых пользователей! Но это уже совсем другая гипотеза...

Заберите с собой

Чтобы ничего не забыть, скачайте шпаргалку и конспект темы.

Где ещё почитать про категоризацию данных

Полезные приёмы библиотеки Pandas

Проверочные задания. Категоризация данных

Чтобы пройти тест нужно правильно ответить на 4 вопроса из 8.

Время на прохождение: 10 минут

Задание 1 из 8
Информацию о категориях иногда хранят внутри датафрейма, не создавая отдельную таблицу. В чём недостатки такого способа? Выберите несколько вариантов.

Правильный ответ
Такие данные занимают больше места, и их долго обрабатывать.

Правильный ответ
Обновлять данные о категориях слишком долго.

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

Задание 2 из 8
Что входит в категоризацию данных на этапе предобработки?

Объединение числовых значений в группы-диапазоны

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

Задание 3 из 8
Сотрудники интернет-магазина ведут учёт жалоб на доставку:
в колонке order_id указан номер заказа;
в колонке type_message — тип жалобы;
в колонке type_id — идентификатор жалобы;
в колонке timestamp — время обращения.
Но так хранить информацию неудобно. Как можно оптимизировать данные? 
   order_id               type_message  type_id             timestamp
0  87645733  Товар не доставили в срок        2   2021-03-28 13:58:24
1  95736342      Прислали не тот товар        3   2021-03-08 14:11:59
2  93746625   Нарушена упаковка товара        5   2021-03-03 18:52:34
3  96573547   Нарушена упаковка товара        5   2021-03-16 19:18:21
4  97365575    Прислали товар с браком        4   2021-03-13 11:43:14
5  95475838  Товар не доставили в срок        2   2021-03-29 17:32:17
6  98485626      Прислали не тот товар        3   2021-04-08 12:37:15
7  83547487         Товар не доставили        1   2021-04-06 20:15:20
8  84646255         Товар не доставили        1   2021-04-09 17:30:43
9  94756363    Прислали товар с браком        4   2021-04-16 21:26:16 


Создать отдельную таблицу с колонками type_id и type_message.

В колонках type_id и type_message — категориальные значения. Поэтому нет смысла объединять их в группы на основе чисел. От колонки type_message в основной таблице лучше избавиться, но сразу удалять её не стоит — как тогда разобраться, на что жалуются пользователи? Тип жалобы с идентификатором можно сохранить в отдельной таблице.

Задание 4 из 8
В этом датафрейме хранятся данные о разных электростанциях:
в колонке country указана страна, где находится электростанция;
в колонке name — название;
в колонке capacity_mw — мощность электростанции в мегаваттах.
           country                 name  capacity_mw
30      Algeria          Kahrama IPP        345.0
31      Algeria    Koudiet Eddraouch       1200.0
32      Algeria               Labreg        396.0
33      Algeria               Marsat        840.0
34      Algeria            Marsat TG        184.0
35      Algeria              Msila 1        980.0
36      Algeria           Ras Djinet        672.0
37      Algeria          Ravin Blanc         73.0
38      Algeria             Relizane        465.0
39      Algeria              SKB IPP        484.0
40      Algeria  SKS IPP SNC Lavalin        815.0 
Такие данные сложно сравнивать, поэтому электростанции можно объединить в группы в зависимости от их мощности. Как выделить такие группы?

Правильный ответ
Написать функцию, которая возвращает категорию по значению мощности.

Группировка поможет сравнить данные, главное — выбрать признак, по которому группировать. Колонка name для этого не подойдёт — все значения в ней уникальны. В колонке capacity_mw много единичных значений, но их удобно объединять в категории. Так можно выделить группы по значению мощности.

Задание 5 из 8
В датафрейм добавили новые колонки — в них содержится информация о том, сколько энергии произвела каждая электростанция в 2013 и 2014 годах. Какие функции посчитают среднее количество произведённой энергии?
        country         name  generation_2013   generation_2014
30      Algeria   Ighil Emda             33.0              27.4
31      Algeria  Kahrama IPP           1440.4            1059.5
32      Algeria       Labreg            393.0             394.9
33      Algeria       Marsat            839.1             810.5
34      Algeria    Marsat TG            181.9             180.4
35      Algeria        Naâma             30.9              29.6
36      Algeria   Ras Djinet           1560.5            1200.2
37      Algeria  Ras el Oued             19.0              20.0
38      Algeria  Ravin Blanc             73.2              70.6
39      Algeria      Reggane              5.0               5.0
40      Algeria      SKS IPP            480.7             450.1 


Правильный ответ
def avg_generation(row):
    avg = (row['generation_2013'] + row['generation_2014']) / 2
    return avg 

Правильный ответ
def avg_generation(row):
    generation2013 = row['generation_2013']
    generation2014 = row['generation_2014']
    avg = (generation2013 + generation2014) / 2
    return avg 

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

Задание 6 из 8
Датафрейм clients_df хранит информацию о покупателях магазина. Чтобы выделить категории в данных, написали функцию categorize_age(). К каким элементам датафрейма clients_df применили эту функцию? 
clients_df['age_category'] = clients_df.apply(categorize_age, axis=1) 

Правильный ответ
К каждой строке датафрейма

Таблица в pandas — двумерный объект. У него есть две координаты: строка и столбец. Перебирать элементы в цикле не стоит — это неоптимальный путь, который займёт много времени. Выбором строки или столбца управляет параметр axis. Чтобы применить метод к строкам, в аргументе указывают axis со значением 1.

Задание 7 из 8
К каким элементам датафрейма применили функцию calc_average()?
average_df = clients_df.apply(calc_average, axis=0) 

Правильный ответ
К каждому столбцу датафрейма

Параметр axis позволит применить код к нужным элементам: строкам или столбцам. Он вам ещё не раз пригодится, поэтому запомните, как его применять.

Задание 8 из 8
К какому элементу датафрейма применили функцию categorize_age()?
clients_df['age_category'] = clients_df['age'].apply(categorize_age) 

Правильный ответ
К столбцу age

Можно по-разному написать функцию, которая выделяет возрастные группы. Функция может принимать на вход строки, тогда её вызывают для всего датафрейма построчно с помощью аргумента axis=1. Если функция принимает на вход значения age, то и применять её можно к отдельному столбцу.

Следующая тема: ПД. Системное и критическое мышление в работе аналитика

Вернуться к разделу: "Предобработка данных"

Вернуться в оглавление: Я.Практикум