Раздел 1 · 4 балла
Данные, pandas и SQL
Читаем три файла и отвечаем на два вопроса. Каждый вопрос решаем дважды: командами pandas и запросом SQL. Склеивать таблицы (join) здесь запрещено.
Что за инструменты
- Таблица (DataFrame)
- Строки это клиенты, столбцы это их свойства. В pandas таблица называется DataFrame.
- pandas
- Библиотека для таблиц. Сокращённо
pd. - SQL
- Язык вопросов к базе данных, читается почти как английский: «SELECT MIN(income) FROM people» = «выбери минимальный доход из people».
- sqlite3
- Крошечная база данных, встроенная в Python. Ноутбук создаёт её в памяти (
':memory:') и кладёт туда наши три таблицы. - CSV
- Текстовый файл-таблица: одна строка = один клиент, значения разделены каким-то символом.
- Пропуск (NaN)
- Пустая ячейка. NaN значит «не число», то есть значения нет.
Три таблицы
| Таблица | Что в ней | Размер | Разделитель |
|---|---|---|---|
people | анкета: год рождения, образование, семья, доход, дети (kidhome), подростки (teenhome), дата регистрации, дни с последней покупки, жалобы | 2240 × 10 | ; |
products | сколько потрачено за 2 года на вино, фрукты, мясо, рыбу, сладости, золото (столбцы mnt...) | 2240 × 7 | табуляция |
purchases | сколько покупок через сайт, по каталогу, в магазине, со скидкой, сколько заходов на сайт (столбцы num...) | 2240 × 6 | , |
Таблицы связаны столбцом id (номер клиента): у одного человека один и тот же id во всех трёх.
Ячейка 1: читаем данные
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns # sns нужен ниже, в ячейке с PCA / t-SNE / UMAP
# у файлов разные разделители: people - ";", products - табуляция, purchases - ","
people = pd.read_csv('people.csv', sep=';')
products = pd.read_csv('products.csv', sep='\t')
purchases = pd.read_csv('purchases.csv')
for name, table in [('people', people), ('products', products), ('purchases', purchases)]:
missing = table.isna().sum()
print(f'{name}: {table.shape[0]} строк, {table.shape[1]} столбцов')
print(' пропуски:', missing[missing > 0].to_dict() if missing.sum() > 0 else 'нет')
import pandas as pdподключаем pandas под коротким именемpd.import seaborn as snsподключаем библиотеку графиков. В исходном ноутбуке её забыли, а ниже она используется.pd.read_csv('people.csv', sep=';')читаем файл в таблицу.sepговорит, каким символом разделены значения.sep='\t'у products разделитель табуляция (невидимый «большой пробел»).pd.read_csv('purchases.csv')безsep: по умолчанию pandas ждёт запятую, она тут и стоит.for name, table in [...]повторяем одно и то же для трёх таблиц по очереди.table.isna().sum()isna()помечает пустые ячейки как True,sum()считает пометки в каждом столбце. Получаем число пропусков по столбцам.table.shapeразмер таблицы: пара (строки, столбцы).[0]строки,[1]столбцы.missing[missing > 0]оставляем только столбцы, где пропуски есть, и печатаем.
Результат:
people: 2240 строк, 10 столбцов
пропуски: {'income': 24}
products: 2240 строк, 7 столбцов
пропуски: нет
purchases: 2240 строк, 6 столбцов
пропуски: нет
people.csv без sep=';', pandas не найдёт запятых и положит каждую строку целиком в один столбец. Получится таблица 2240 × 1, и пропусков в доходе не будет видно. Это главный подвох первого задания.sns.scatterplot, но import seaborn as sns в ноутбуке нигде нет. Без нашей строки там будет ошибка NameError: name 'sns' is not defined.Задание 1: разница доходов
Вопрос: абсолютная разница между минимальным доходом клиентов с детьми и максимальным доходом клиентов без детей. Учитываем только kidhome.
- Группа «с детьми»:
kidhome > 0. Самый маленький доход в ней: 2447. - Группа «без детей»:
kidhome == 0. Самый большой доход в ней: 160803. - Разница по модулю: |2447 − 160803| = 158356.
- Модуль, abs
- Число без знака минус: abs(−5) = 5. «Абсолютная разница» = разница по модулю.
- Маска
- Столбец из True / False: True там, где условие выполнено. По маске выбирают нужные строки.
pandas
min_income_with_kids = people.loc[people['kidhome'] > 0, 'income'].min()
max_income_without_kids = people.loc[people['kidhome'] == 0, 'income'].max()
abs(min_income_with_kids - max_income_without_kids)
people['kidhome'] > 0маска: True у тех, у кого есть дети.people.loc[маска, 'income']locберёт строки по маске и столбецincome. Получаем доходы только семей с детьми..min()наименьший из них, 2447. Пропуски (NaN)minсам пропускает.== 0двойное «равно» это вопрос «равно ли?». Одинарное «=» значит «положить значение в переменную».abs(...)модуль разности. Последнюю строку ячейки Colab выводит сам:158356.0.
SQL
query = """
SELECT ABS(
(SELECT MIN(income) FROM people WHERE kidhome > 0)
- (SELECT MAX(income) FROM people WHERE kidhome = 0)
) AS income_diff;
"""
pd.read_sql_query(query, conn)
"""..."""тройные кавычки: текст запроса можно писать на нескольких строках.(SELECT MIN(income) FROM people WHERE kidhome > 0)подзапрос, то есть маленький запрос внутри большого. Возвращает одно число, 2447.(SELECT MAX(income) ... WHERE kidhome = 0)второй подзапрос, 160803. В SQL одно «=» это сравнение.ABS(...) AS income_diffмодуль разности, аASдаёт столбцу ответа имя.pd.read_sql_query(query, conn)отправляет запрос в базуconnи возвращает ответ таблицей:158356.0.
Задание 2: доля PhD среди любителей сладкого
Вопрос: делим клиентов на тех, кто покупает сладкое, и тех, кто не покупает. В каждой группе считаем, какая часть имеет PhD, а какая другое образование. Сумма по строке = 1.
- PhD
- Учёная степень (кандидат наук). Other = все остальные: Graduation, Master, 2n Cycle, Basic.
- «Покупает сладкое»
- За 2 года потратил на сладости больше нуля:
mntsweetproducts > 0. - Доля
- Часть от группы: (сколько PhD в группе) / (сколько всего людей в группе). Доля 0.18 = 18%.
Сложность: доход и образование лежат в people, а сладости в products. Склеивать нельзя, поэтому сначала достаём список id сладкоежек, а потом спрашиваем у каждого человека из people, есть ли он в этом списке.
pandas
# id клиентов, которые хоть раз тратили деньги на сладкое
sweet_ids = products.loc[products['mntsweetproducts'] > 0, 'id']
sweets_group = people['id'].isin(sweet_ids).map({True: 'Sweets', False: 'No Sweets'})
education_group = (people['education'] == 'PhD').map({True: 'PhD', False: 'Other'})
# normalize='index' - делим на сумму по строке, поэтому строка в сумме даёт 1
answer = pd.crosstab(sweets_group, education_group, normalize='index')
answer = answer.loc[['Sweets', 'No Sweets'], ['PhD', 'Other']]
answer.index.name = None
answer.columns.name = None
answer
sweet_ids = products.loc[...]список номеров клиентов, у которых траты на сладкое больше нуля.people['id'].isin(sweet_ids)для каждого человека: есть ли его номер в списке (True / False). Это замена join..map({True: 'Sweets', False: 'No Sweets'})переименовываем True / False в понятные слова по словарю.(people['education'] == 'PhD').map(...)то же для образования: «PhD» или «Other».pd.crosstab(a, b)таблица-счётчик: сколько людей в каждой паре групп (Sweets + PhD и т.д.).normalize='index'делит каждую строку на её сумму: из количеств получаются доли.answer.loc[['Sweets', 'No Sweets'], ['PhD', 'Other']]ставим строки и столбцы в порядке как в задании (сам crosstab сортирует по алфавиту).index.name = Noneубираем лишние подписи над таблицей.
SQL
query = """
SELECT
CASE WHEN id IN (SELECT id FROM products WHERE mntsweetproducts > 0)
THEN 'Sweets' ELSE 'No Sweets' END AS sweets,
AVG(CASE WHEN education = 'PhD' THEN 1.0 ELSE 0.0 END) AS PhD,
AVG(CASE WHEN education <> 'PhD' THEN 1.0 ELSE 0.0 END) AS Other
FROM people
GROUP BY sweets
ORDER BY sweets DESC;
"""
pd.read_sql_query(query, conn).set_index('sweets').rename_axis(None)
id IN (SELECT id FROM products WHERE ...)«есть ли номер человека среди сладкоежек». Аналогisin.CASE WHEN ... THEN 'Sweets' ELSE 'No Sweets' ENDэто «если ..., то ..., иначе ...». Каждому человеку пишем его группу.CASE WHEN education = 'PhD' THEN 1.0 ELSE 0.0 ENDу PhD ставим 1, у остальных 0.AVG(...)среднее из единиц и нулей это и есть доля единиц. Пример: 1, 0, 0, 0 → среднее 0.25 → 25% PhD.<>в SQL значит «не равно».GROUP BY sweetsсчитаем средние отдельно для группы Sweets и отдельно для No Sweets.ORDER BY sweets DESCсортируем по убыванию: «S» по алфавиту дальше «N», поэтому Sweets встаёт первой строкой..set_index('sweets')делаем названия групп подписями строк, чтобы таблица выглядела как в задании.
Ответ (pandas и SQL совпали)
| PhD | Other | |
|---|---|---|
| Sweets | 0.182867 | 0.817133 |
| No Sweets | 0.365155 | 0.634845 |
Проверка: 0.182867 + 0.817133 = 1. Среди тех, кто сладкое не покупает, доля PhD 36.5%, это вдвое больше, чем у сладкоежек (18.3%).
Вопросы на защите
Сколько строк и столбцов, есть ли пропуски?
Все таблицы по 2240 строк; people 10 столбцов, products 7, purchases 6. Пропуски только в income, их 24.
Почему у read_csv разные sep?
Файлы сохранены с разными разделителями: ;, табуляция и запятая. Без правильного sep таблица превращается в один столбец текста.
Как ты обошёлся без join?
В pandas через isin: проверяю, есть ли id человека в списке сладкоежек. В SQL через подзапрос id IN (SELECT id FROM products ...).
Почему AVG от нулей и единиц даёт долю?
Среднее = сумма / количество. Сумма единиц это число PhD, количество это размер группы. Значит среднее = доля PhD.
Почему строка в сумме 1?
Каждый человек группы либо PhD, либо Other. Две доли вместе покрывают всю группу, то есть 100%.