Домашнее задание. NumPy, pandas, CSV и JSON¶

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

Как решать

  • В каждой задаче уже есть заготовка функции. Замените raise NotImplementedError своим решением.
  • Не меняйте названия функций и их аргументы: решения будут проверяться автоматически.
  • Функция должна возвращать ответ, а не печатать его. Входные массивы и таблицы не изменяйте.
  • После каждой задачи есть ячейка с проверкой на примерах. Если она печатает «OK», решение верно хотя бы на этих примерах. Скрытые тесты будут другими, поэтому учитывайте все условия.
  • В задачах на NumPy старайтесь обходиться без циклов for: почти всё решается одной-двумя строками.

Баллы

Часть Задачи Баллы
NumPy 1–10 по 0.5, всего 5
pandas, CSV и JSON 11–14 по 1, всего 4
Итого 9

Как сдать

Решения сдаются в Яндекс.Контесте.

  1. Сохраните ноутбук и отправьте весь файл .ipynb целиком в задачу «1. NumPy» и тот же файл — в задачу «2. Pandas». Компилятор — (make) yandexdataschool.
  2. Проверка занимает несколько минут. В отчёте видно, какие задачи засчитаны, а для незасчитанных указана причина. Можно сдать часть задач, а потом отправить ноутбук ещё раз.
  3. При проверке выполняются только импорты и определения функций: остальные ячейки (примеры, проверки, print) не запускаются, поэтому незаконченная задача или упавший assert не мешают проверить остальные. Но функция не должна использовать переменные из других ячеек — всё нужное передаётся ей в аргументах.
  4. Задачи 3–8 по математике (векторы и матрицы) решаются на бумаге, ответ вводится прямо в Контесте. Формат ответа описан в условии; на каждую задачу 5 попыток.

Автор: Алексей Чернуха. CC BY 4.0.

In [1]:
import json

import numpy as np
import pandas as pd

Шпаргалка¶

Перед задачами вспомним функции, которые пригодятся в этом задании. Запустите ячейки и посмотрите, что они выводят. Если нужна подробность, загляните в документацию: NumPy, pandas.

NumPy: создание массивов и срезы¶

In [2]:
print(np.arange(1, 10, 3))          # от 1 до 10 (не включая) с шагом 3
print(np.zeros(4), np.ones(3, dtype=int))
print(np.full(3, 7))                 # массив из одинаковых чисел

m = np.arange(12).reshape(3, 4)      # матрица 3 × 4
print(m)
print("строка 1:      ", m[1])
print("столбец 2:     ", m[:, 2])
print("каждая вторая: ", m[0, ::2])  # срез с шагом
print("форма:", m.shape, "| тип:", m.dtype)
[1 4 7]
[0. 0. 0. 0.] [1 1 1]
[7 7 7]
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
строка 1:       [4 5 6 7]
столбец 2:      [ 2  6 10]
каждая вторая:  [0 2]
форма: (3, 4) | тип: int64

NumPy: операции, маски и агрегаты по осям¶

Арифметика и сравнения работают поэлементно. Массив из True/False (маска) выбирает элементы; True при сложении считается единицей.

In [3]:
a = np.array([4, -1, 7, 2, -5])
print(a * 10, a ** 2, np.abs(a))
print("маска a > 0:   ", a > 0)
print("элементы > 0:  ", a[a > 0])
print("сколько > 0:   ", (a > 0).sum())
print("и / или:       ", a[(a > 0) & (a < 5)], a[(a < 0) | (a > 5)])

m = np.array([[1, 2, 3],
              [4, 5, 6]])
print("сумма всего:        ", m.sum())
print("среднее по столбцам:", m.mean(axis=0))   # axis=0 — сворачиваем строки
print("среднее по строкам: ", m.mean(axis=1))   # axis=1 — сворачиваем столбцы
print("с keepdims:\n", m.mean(axis=1, keepdims=True))
[ 40 -10  70  20 -50] [16  1 49  4 25] [4 1 7 2 5]
маска a > 0:    [ True False  True  True False]
элементы > 0:   [4 7 2]
сколько > 0:    3
и / или:        [4 2] [-1  7 -5]
сумма всего:         21
среднее по столбцам: [2.5 3.5 4.5]
среднее по строкам:  [2. 5.]
с keepdims:
 [[2.]
 [5.]]

NumPy: broadcasting¶

Массивы разной формы растягиваются друг под друга: строка прибавляется к каждой строке матрицы, столбец — к каждому столбцу.

In [4]:
m = np.array([[1, 2, 3],
              [4, 5, 6]])
print(m + np.array([10, 20, 30]))      # строка формы (3,)
print(m + np.array([[100], [200]]))    # столбец формы (2, 1)
print(np.arange(3).reshape(-1, 1) + np.arange(4))   # (3, 1) + (4,) -> (3, 4)
[[11 22 33]
 [14 25 36]]
[[101 102 103]
 [204 205 206]]
[[0 1 2 3]
 [1 2 3 4]
 [2 3 4 5]]

NumPy: индексы минимума, сортировка, пропуски, выбор по условию¶

In [5]:
a = np.array([30, 10, 20, 10])
print("индекс минимума:", a.argmin(), "| индекс максимума:", a.argmax())  # при равенстве — первый
print("порядок сортировки:", np.argsort(a, kind="stable"))
print("отсортированный:   ", a[np.argsort(a, kind="stable")])
print("по списку индексов:", a[[3, 0, 0]])

b = np.array([1.0, np.nan, 3.0])
print("где пропуск:", np.isnan(b))
print("mean / nanmean:", b.mean(), np.nanmean(b))

print("np.where:", np.where(a > 15, a, -1))   # где условие верно — a, иначе -1
print("единичная матрица:\n", np.eye(3, dtype=int))
индекс минимума: 1 | индекс максимума: 0
порядок сортировки: [1 3 2 0]
отсортированный:    [10 10 20 30]
по списку индексов: [10 30 30]
где пропуск: [False  True False]
mean / nanmean: nan 2.0
np.where: [30 -1 20 -1]
единичная матрица:
 [[1 0 0]
 [0 1 0]
 [0 0 1]]

pandas: чтение и осмотр таблицы¶

pd.read_csv(path) читает CSV-файл. Для примера соберём небольшую таблицу прямо в коде.

In [6]:
students = pd.DataFrame({
    "name":  ["Ася", "Боря", "Вика", "Гена", "Даша"],
    "grade": [10, 11, None, 10, 11],
    "school": ["№1", "№2", "№1", "№1", "№2"],
    "score": [85, 92, 78, 64, 99],
})
print(students.dtypes)               # grade стал float из-за пропуска
students.head()
name       object
grade     float64
school     object
score       int64
dtype: object
Out[6]:
name grade school score
0 Ася 10.0 №1 85
1 Боря 11.0 №2 92
2 Вика NaN №1 78
3 Гена 10.0 №1 64
4 Даша 11.0 №2 99

pandas: фильтрация, пропуски, типы¶

In [7]:
print(students[students["score"] >= 80]["name"].tolist())
print(students[students["score"].between(70, 90)]["name"].tolist())   # границы включаются
print("пропусков:", students["grade"].isna().sum())

known = students.dropna(subset=["grade"]).reset_index(drop=True)      # индекс заново 0, 1, 2, …
known["grade"] = known["grade"].astype(int)
known
['Ася', 'Боря', 'Даша']
['Ася', 'Вика']
пропусков: 1
Out[7]:
name grade school score
0 Ася 10 №1 85
1 Боря 11 №2 92
2 Гена 10 №1 64
3 Даша 11 №2 99

pandas: группировка и сортировка¶

В agg можно сразу задать имена новых столбцов: имя=("столбец", "функция").

In [8]:
by_school = students.groupby("school").agg(
    students=("name", "count"),
    best=("score", "max"),
    avg=("score", "mean"),
)
print(by_school, "\n")

# сначала по убыванию school, при равенстве — по возрастанию score
students.sort_values(["school", "score"], ascending=[False, True])
        students  best        avg
school                           
№1             3    85  75.666667
№2             2    99  95.500000 

Out[8]:
name grade school score
1 Боря 11.0 №2 92
4 Даша 11.0 №2 99
3 Гена 10.0 №1 64
2 Вика NaN №1 78
0 Ася 10.0 №1 85

JSON и объединение таблиц¶

Модуль json читает файл в обычные списки и словари. pd.json_normalize разворачивает вложенные списки в таблицу, а pd.merge объединяет таблицы по общему столбцу.

In [9]:
teams = [
    {"team": "Альфа", "city": "Минск", "members": [{"name": "Ася", "age": 16}, {"name": "Боря", "age": 17}]},
    {"team": "Бета",  "city": "Брест", "members": [{"name": "Вика", "age": 15}]},
]
with open("teams.json", "w", encoding="utf-8") as f:
    json.dump(teams, f, ensure_ascii=False)

with open("teams.json", encoding="utf-8") as f:
    data = json.load(f)
print(type(data), data[0]["team"], data[0]["members"][1]["name"])

members = pd.json_normalize(data, record_path="members", meta=["team"])
print(members, "\n")

pd.merge(members, students[["name", "score"]], on="name")
<class 'list'> Альфа Боря
   name  age   team
0   Ася   16  Альфа
1  Боря   17  Альфа
2  Вика   15   Бета 

Out[9]:
name age team score
0 Ася 16 Альфа 85
1 Боря 17 Альфа 92
2 Вика 15 Бета 78

Часть 1. NumPy¶

Задача 1. Квадраты чётных чисел (0.5 балла)¶

Верните массив квадратов всех чётных чисел от 0 до n включительно, по возрастанию.

even_squares(7)  ->  [0, 4, 16, 36]
even_squares(0)  ->  [0]
In [ ]:
def even_squares(n: int) -> np.ndarray:
    # ваш код
    raise NotImplementedError
In [ ]:
assert np.array_equal(even_squares(7), [0, 4, 16, 36])
assert np.array_equal(even_squares(8), [0, 4, 16, 36, 64])
assert np.array_equal(even_squares(0), [0])
print("Задача 1: OK")

Задача 2. Шахматная доска (0.5 балла)¶

Верните матрицу n × n из нулей и единиц, раскрашенную как шахматная доска. В левом верхнем углу стоит 0.

checkerboard(3)  ->  [[0, 1, 0],
                      [1, 0, 1],
                      [0, 1, 0]]
In [ ]:
def checkerboard(n: int) -> np.ndarray:
    # ваш код
    raise NotImplementedError
In [ ]:
assert np.array_equal(checkerboard(3), [[0, 1, 0], [1, 0, 1], [0, 1, 0]])
assert np.array_equal(checkerboard(2), [[0, 1], [1, 0]])
assert np.array_equal(checkerboard(1), [[0]])
assert checkerboard(8).shape == (8, 8) and checkerboard(8).sum() == 32
print("Задача 2: OK")

Задача 3. Нормировка в отрезок [0, 1] (0.5 балла)¶

Признаки часто приводят к одному масштабу. Отобразите значения массива x в отрезок $[0, 1]$: минимум переходит в 0, максимум в 1:

$$x' = \frac{x - \min x}{\max x - \min x}.$$

Если все элементы равны, верните массив из нулей той же длины.

minmax_normalize([2, 4, 6, 10])  ->  [0.0, 0.25, 0.5, 1.0]
minmax_normalize([5, 5, 5])      ->  [0.0, 0.0, 0.0]
In [ ]:
def minmax_normalize(x: np.ndarray) -> np.ndarray:
    # ваш код
    raise NotImplementedError
In [ ]:
assert np.allclose(minmax_normalize(np.array([2, 4, 6, 10])), [0, 0.25, 0.5, 1])
assert np.allclose(minmax_normalize(np.array([-1.0, 1.0])), [0, 1])
assert np.allclose(minmax_normalize(np.array([5, 5, 5])), [0, 0, 0])
print("Задача 3: OK")

Задача 4. Ближайшее значение (0.5 балла)¶

Найдите в массиве x элемент, ближайший к числу v. Если таких несколько, верните тот, что стоит в массиве раньше.

nearest_value([1, 8, 3, 10], 7)  ->  8
nearest_value([4, 6], 5)         ->  4
In [ ]:
def nearest_value(x: np.ndarray, v: float) -> float:
    # ваш код
    raise NotImplementedError
In [ ]:
assert nearest_value(np.array([1, 8, 3, 10]), 7) == 8
assert nearest_value(np.array([4, 6]), 5) == 4
assert nearest_value(np.array([6, 4]), 5) == 6
assert nearest_value(np.array([0.5, -2.0, 3.0]), -10) == -2.0
print("Задача 4: OK")

Задача 5. Сортировка строк по столбцу (0.5 балла)¶

Дана матрица m: строки — объекты, столбцы — признаки. Отсортируйте строки по возрастанию значения в столбце с номером j. Строки с одинаковым значением должны остаться в исходном порядке.

m = [[3, 10],
     [1, 20],
     [2, 30]]
sort_rows_by_column(m, 0)  ->  [[1, 20], [2, 30], [3, 10]]

Подсказка: np.argsort(..., kind="stable").

In [ ]:
def sort_rows_by_column(m: np.ndarray, j: int) -> np.ndarray:
    # ваш код
    raise NotImplementedError
In [ ]:
m = np.array([[3, 10], [1, 20], [2, 30]])
assert np.array_equal(sort_rows_by_column(m, 0), [[1, 20], [2, 30], [3, 10]])
assert np.array_equal(sort_rows_by_column(m, 1), m)
m = np.array([[1, 5], [0, 7], [1, 2], [0, 1]])
assert np.array_equal(sort_rows_by_column(m, 0), [[0, 7], [0, 1], [1, 5], [1, 2]])
print("Задача 5: OK")

Задача 6. Центрирование строк (0.5 балла)¶

Вычтите из каждой строки матрицы m среднее значение этой строки. После этого среднее каждой строки станет равно нулю.

center_rows([[1, 2, 3],
             [10, 20, 60]])  ->  [[-1, 0, 1],
                                  [-20, -10, 30]]

Подсказка: mean(axis=1, keepdims=True).

In [ ]:
def center_rows(m: np.ndarray) -> np.ndarray:
    # ваш код
    raise NotImplementedError
In [ ]:
m = np.array([[1, 2, 3], [10, 20, 60]])
assert np.allclose(center_rows(m), [[-1, 0, 1], [-20, -10, 30]])
assert np.allclose(center_rows(np.array([[5.0, 5.0]])), [[0, 0]])
print("Задача 6: OK")

Задача 7. Заполнение пропусков (0.5 балла)¶

Как говорилось на лекции, в реальных данных бывают пропуски. В матрице m они обозначены как np.nan. Замените каждый пропуск средним значением его столбца, посчитанным по известным значениям. Гарантируется, что в каждом столбце есть хотя бы одно число. Исходную матрицу не изменяйте.

m = [[1,   nan],
     [3,   4  ],
     [nan, 8  ]]
fill_nan_with_column_mean(m)  ->  [[1, 6],
                                   [3, 4],
                                   [2, 8]]

Подсказка: np.nanmean, np.isnan, np.where.

In [ ]:
def fill_nan_with_column_mean(m: np.ndarray) -> np.ndarray:
    # ваш код
    raise NotImplementedError
In [ ]:
m = np.array([[1, np.nan], [3, 4], [np.nan, 8]])
original = m.copy()
assert np.allclose(fill_nan_with_column_mean(m), [[1, 6], [3, 4], [2, 8]])
assert np.array_equal(m, original, equal_nan=True), "исходная матрица изменилась"
assert np.allclose(fill_nan_with_column_mean(np.array([[1.0, 2.0]])), [[1, 2]])
print("Задача 7: OK")

Задача 8. One-hot кодирование (0.5 балла)¶

Категориальный признак (например, город или профессию) часто превращают в набор бинарных признаков. Дан массив labels с номерами категорий от 0 до n_classes - 1. Верните матрицу размера len(labels) × n_classes из нулей и единиц: в строке i единица стоит только в столбце labels[i].

one_hot([2, 0, 1, 2], 3)  ->  [[0, 0, 1],
                               [1, 0, 0],
                               [0, 1, 0],
                               [0, 0, 1]]
In [ ]:
def one_hot(labels: np.ndarray, n_classes: int) -> np.ndarray:
    # ваш код
    raise NotImplementedError
In [ ]:
assert np.array_equal(one_hot(np.array([2, 0, 1, 2]), 3),
                      [[0, 0, 1], [1, 0, 0], [0, 1, 0], [0, 0, 1]])
assert np.array_equal(one_hot(np.array([0, 0]), 4), [[1, 0, 0, 0], [1, 0, 0, 0]])
print("Задача 8: OK")

Задача 9. Ошибки регрессии (0.5 балла)¶

Посчитайте две метрики из лекции для истинных ответов y_true и предсказаний y_pred:

$$\mathrm{MAE}=\frac1N\sum_{i=1}^{N}\lvert y_{\text{pred},i}-y_{\text{true},i}\rvert, \qquad \mathrm{MSE}=\frac1N\sum_{i=1}^{N}\bigl(y_{\text{pred},i}-y_{\text{true},i}\bigr)^2.$$

Верните пару (mae, mse).

Пример из лекции: цены квартир 5, 6, 7, 8, 24 млн, всем предсказываем медиану 7.

regression_errors([5, 6, 7, 8, 24], [7, 7, 7, 7, 7])  ->  (4.2, 59.0)
In [ ]:
def regression_errors(y_true: np.ndarray, y_pred: np.ndarray) -> tuple[float, float]:
    # ваш код
    raise NotImplementedError
In [ ]:
mae, mse = regression_errors(np.array([5, 6, 7, 8, 24]), np.array([7, 7, 7, 7, 7]))
assert np.isclose(mae, 4.2) and np.isclose(mse, 59.0)
mae, mse = regression_errors(np.array([5, 6, 7, 8, 24]), np.full(5, 10))
assert np.isclose(mae, 5.6) and np.isclose(mse, 50.0)
print("Задача 9: OK")

Задача 10. Accuracy, precision, recall (0.5 балла)¶

Модель отвечает «да» (1) или «нет» (0). По истинным ответам y_true и предсказаниям y_pred посчитайте метрики из лекции:

  • accuracy — доля верных ответов среди всех;
  • precision — доля верных среди тех, кому модель сказала «да»;
  • recall — доля найденных моделью среди всех настоящих «да».

Если модель ни разу не сказала «да», считайте precision равным 0. Если настоящих «да» нет, считайте recall равным 0.

Верните тройку (accuracy, precision, recall).

y_true = [1, 0, 1, 1, 0, 0, 1, 0]
y_pred = [1, 1, 0, 1, 1, 0, 1, 0]
binary_metrics(y_true, y_pred)  ->  (0.625, 0.6, 0.75)
In [ ]:
def binary_metrics(y_true: np.ndarray, y_pred: np.ndarray) -> tuple[float, float, float]:
    # ваш код
    raise NotImplementedError
In [ ]:
y_true = np.array([1, 0, 1, 1, 0, 0, 1, 0])
y_pred = np.array([1, 1, 0, 1, 1, 0, 1, 0])
assert np.allclose(binary_metrics(y_true, y_pred), (0.625, 0.6, 0.75))
assert np.allclose(binary_metrics(np.array([1, 0, 1]), np.array([0, 0, 0])), (1 / 3, 0, 0))
assert np.allclose(binary_metrics(np.array([0, 0]), np.array([0, 1])), (0.5, 0, 0))
print("Задача 10: OK")

Часть 2. pandas, CSV и JSON¶

Работаем с данными банка о клиентах и интернет-магазина о заказах. Ячейка ниже создаёт два файла с примерами:

  • clients.csv — клиенты банка: возраст, город, профессия, доход в рублях и вернул ли клиент кредит (returned: 1 — да, 0 — нет). Как и в таблице из лекции, здесь есть пропуски и ошибки в данных.
  • orders.json — заказы этих клиентов в интернет-магазине: у каждого заказа есть список товаров с ценой и количеством.

Функции получают путь к файлу: проверка будет запускать их на других файлах той же структуры.

In [10]:
clients_csv = """client_id,name,age,city,profession,income,returned
1,Анна,34,Минск,врач,2100,1
2,Борис,,Гомель,инженер,1650,0
3,Виктор,140,Минск,учитель,1900,1
4,Галина,29,Брест,программист,52000,1
5,Дмитрий,41,Гомель,инженер,1500,1
6,Елена,23,Минск,программист,2600,0
7,Жанна,57,Брест,врач,2300,1
8,Захар,15,Минск,школьник,0,1
9,Ирина,36,Гомель,учитель,1400,0
10,Кирилл,48,Минск,инженер,2000,1
"""
with open("clients.csv", "w", encoding="utf-8") as f:
    f.write(clients_csv)

orders = [
    {"order_id": 101, "client_id": 1, "date": "2026-09-01",
     "items": [{"product": "тетрадь", "price": 2.5, "qty": 4},
               {"product": "ручка", "price": 1.2, "qty": 10}]},
    {"order_id": 102, "client_id": 4, "date": "2026-09-02",
     "items": [{"product": "ноутбук", "price": 2400.0, "qty": 1}]},
    {"order_id": 103, "client_id": 1, "date": "2026-09-05",
     "items": [{"product": "рюкзак", "price": 85.0, "qty": 1},
               {"product": "тетрадь", "price": 2.5, "qty": 2}]},
    {"order_id": 104, "client_id": 6, "date": "2026-09-07", "items": []},
    {"order_id": 105, "client_id": 9, "date": "2026-09-08",
     "items": [{"product": "книга", "price": 30.0, "qty": 3}]},
    {"order_id": 106, "client_id": 6, "date": "2026-09-10",
     "items": [{"product": "наушники", "price": 120.0, "qty": 1},
               {"product": "ручка", "price": 1.2, "qty": 5}]},
]
with open("orders.json", "w", encoding="utf-8") as f:
    json.dump(orders, f, ensure_ascii=False, indent=2)

pd.read_csv("clients.csv")
Out[10]:
client_id name age city profession income returned
0 1 Анна 34.0 Минск врач 2100 1
1 2 Борис NaN Гомель инженер 1650 0
2 3 Виктор 140.0 Минск учитель 1900 1
3 4 Галина 29.0 Брест программист 52000 1
4 5 Дмитрий 41.0 Гомель инженер 1500 1
5 6 Елена 23.0 Минск программист 2600 0
6 7 Жанна 57.0 Брест врач 2300 1
7 8 Захар 15.0 Минск школьник 0 1
8 9 Ирина 36.0 Гомель учитель 1400 0
9 10 Кирилл 48.0 Минск инженер 2000 1

Задача 11. Чистка данных (1 балл)¶

Прочитайте таблицу клиентов из CSV-файла по пути path и удалите строки с некорректным возрастом:

  • возраст не указан (пропуск);
  • возраст меньше 18 или больше 100.

Верните таблицу с теми же столбцами в исходном порядке строк. Индекс должен идти заново: 0, 1, 2, … (reset_index(drop=True)). Столбец age должен иметь целочисленный тип (int): после пропусков pandas читает его как дробный.

На примере остаются клиенты с client_id 1, 4, 5, 6, 7, 9, 10.

In [ ]:
def clean_clients(path: str) -> pd.DataFrame:
    # ваш код
    raise NotImplementedError
In [ ]:
clean = clean_clients("clients.csv")
assert list(clean.columns) == ["client_id", "name", "age", "city", "profession", "income", "returned"]
assert clean["client_id"].tolist() == [1, 4, 5, 6, 7, 9, 10]
assert clean.index.tolist() == list(range(7))
assert pd.api.types.is_integer_dtype(clean["age"]), "age должен быть целым"
assert clean["age"].tolist() == [34, 29, 41, 23, 57, 36, 48]
print("Задача 11: OK")

Задача 12. Отчёт по городам (1 балл)¶

По очищенной таблице клиентов (используйте clean_clients(path)) постройте отчёт по городам. Верните таблицу, у которой индекс — название города (city), а столбцы такие:

Столбец Что в нём
clients число клиентов из города
mean_income средний доход
return_rate доля клиентов, вернувших кредит

Строки отсортируйте по убыванию clients, а при равенстве — по названию города в алфавитном порядке.

На примере:

city clients mean_income return_rate
Минск 3 2233.33… 0.666…
Брест 2 27150.0 1.0
Гомель 2 1450.0 0.5

Подсказка: groupby(...).agg(clients=("client_id", "count"), ...).

In [ ]:
def city_report(path: str) -> pd.DataFrame:
    # ваш код
    raise NotImplementedError
In [ ]:
report = city_report("clients.csv")
expected = pd.DataFrame(
    {"clients": [3, 2, 2],
     "mean_income": [6700 / 3, 27150.0, 1450.0],
     "return_rate": [2 / 3, 1.0, 0.5]},
    index=pd.Index(["Минск", "Брест", "Гомель"], name="city"),
)
pd.testing.assert_frame_equal(report, expected, check_dtype=False, check_index_type=False)
print("Задача 12: OK")

Задача 13. Суммы заказов из JSON (1 балл)¶

Прочитайте заказы из JSON-файла по пути path (структура как в orders.json). Посчитайте сумму каждого заказа: сумму price × qty по всем его товарам. У заказа без товаров сумма равна 0.

Верните pd.Series с именем total, у которой индекс — order_id (с именем order_id), отсортированный по возрастанию.

order_id
101     22.0
102   2400.0
103     90.0
104      0.0
105     90.0
106    126.0
Name: total, dtype: float64

Подсказка: файл можно прочитать модулем json, а вложенный список товаров развернуть через pd.json_normalize(data, record_path="items", meta=[...]). Посмотрите, что при этом происходит с заказом без товаров.

In [ ]:
def order_totals(path: str) -> pd.Series:
    # ваш код
    raise NotImplementedError
In [ ]:
totals = order_totals("orders.json")
expected = pd.Series([22.0, 2400.0, 90.0, 0.0, 90.0, 126.0], name="total",
                     index=pd.Index([101, 102, 103, 104, 105, 106], name="order_id"))
pd.testing.assert_series_equal(totals, expected, check_dtype=False, check_index_type=False)
print("Задача 13: OK")

Задача 14. Лучшие покупатели (1 балл)¶

Объедините клиентов из CSV (clients_path, без очистки) и заказы из JSON (orders_path). Для каждого клиента, сделавшего хотя бы один заказ, посчитайте общую сумму всех его заказов. Верните k клиентов с наибольшей суммой.

Результат — таблица со столбцами name и total, отсортированная по убыванию total, а при равенстве — по имени в алфавитном порядке. Индекс: 0, 1, …, k - 1. Если клиентов с заказами меньше k, верните всех. Гарантируется, что у каждого заказа client_id есть в таблице клиентов.

На примере top_clients("clients.csv", "orders.json", 3):

name total
0 Галина 2400.0
1 Елена 126.0
2 Анна 112.0

Подсказка: используйте order_totals и pd.merge.

In [ ]:
def top_clients(clients_path: str, orders_path: str, k: int) -> pd.DataFrame:
    # ваш код
    raise NotImplementedError
In [ ]:
top = top_clients("clients.csv", "orders.json", 3)
expected = pd.DataFrame({"name": ["Галина", "Елена", "Анна"], "total": [2400.0, 126.0, 112.0]})
pd.testing.assert_frame_equal(top, expected, check_dtype=False, check_index_type=False)
top = top_clients("clients.csv", "orders.json", 10)
assert top["name"].tolist() == ["Галина", "Елена", "Анна", "Ирина"]
print("Задача 14: OK")