{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Мини-проект: ведомость группы\n",
    "\n",
    "Разобрать оценки 30 студентов по пяти предметам — средние по предметам и студентам, лучшие, рейтинг, кто сдал всё, кто в зоне риска.\n",
    "\n",
    "Курс NumPy · урок `np-project-grades` · [урок на сайте](https://ds-edu-trainer.vercel.app/courses/numpy/project-grades).\n",
    "\n",
    "Выполняйте ячейки по порядку (Shift+Enter), начиная с первой — она готовит урок. После каждого упражнения идёт\n",
    "ячейка проверки: она печатает ✓ / ✗ и итог."
   ],
   "id": "intro"
  },
  {
   "cell_type": "code",
   "metadata": {
    "cellView": "form",
    "jupyter": {
     "source_hidden": true
    }
   },
   "source": [
    "# @title Подготовка урока — выполните эту ячейку первой\n",
    "# Проверяет версии пакетов, записывает файлы данных в папку data/ и определяет _check — проверку упражнений.\n",
    "import base64, os, zlib\n",
    "\n",
    "for _name, _min in {\"numpy\":\"2.0\"}.items():\n",
    "    try:\n",
    "        _have = __import__(_name).__version__\n",
    "    except ImportError:\n",
    "        print(f\"Нет пакета {_name}: выполните %pip install {_name} и перезапустите ядро\")\n",
    "        continue\n",
    "    if tuple(int(x) for x in _have.split(\".\")[:2]) < tuple(int(x) for x in _min.split(\".\")):\n",
    "        print(f\"Урок написан для {_name} {_min} и новее, у вас {_have}: выполните %pip install -U {_name} и перезапустите ядро\")\n",
    "\n",
    "_DATA = {\n",
    "    \"scores.csv\": \"\"\"\n",
    "eNo9kV1uGzEMhN91lnkgKfHvHDpAETSBvUDtBF73IbcPSBcFFgKWGn0cDs/n3/eP+/PX8Y7b2/OKr+v3efw+8fX4vDzebrfjfsH1\n",
    "OJ+fj2983C9/jvM6NjEssLLOEASPTQInmFXJHeFj04QxNOACIziNTQtuMGnVgs+xSeEBz9IGwXNsMpjCHK4tL7xDpXGG5ViFD+iE\n",
    "LahWB19jU3ZJoY5cWHNsJuiCJdaEM0zHZi620T9hqwQxkRPp1ZGJxuZZLkuZ9WmOzQs26413V6uXiiiLDaf2ytbRtAUjqIzN3qzs\n",
    "iRQWY3Mgub1bD1nGsvAVlTSexhaqACtj7Q45tjCCkZ1hZLNE6nJFsxmuY8vsn7Zm6/Wwww9uVrxKCuUKVBma0GIZ/DU5YUlvTWqf\n",
    "XZWaqZKWvtcOx72TkKz9MVEFEtS1Sb2Q/yeNH8ySg9M=\n",
    "\"\"\",\n",
    "}\n",
    "os.makedirs(\"data\", exist_ok=True)\n",
    "for _name, _packed in _DATA.items():\n",
    "    with open(os.path.join(\"data\", _name), \"wb\") as _f:\n",
    "        _f.write(zlib.decompress(base64.b64decode(_packed)))\n",
    "print(\"Файлы данных:\", \", \".join(sorted(_DATA)))\n",
    "\n",
    "# ─── Проверка упражнений: тот же раннер, что у задач на сайте ───\n",
    "# ─── Запуск тестов ───────────────────────────────────────────────────────────\n",
    "\n",
    "_TIMEOUT = 2.0  # секунд на тест; отдельный тест может задать свой: test_x.timeout = 10\n",
    "\n",
    "\n",
    "def _call_with_timeout(target, seconds: float) -> bool:\n",
    "    \"\"\"Запускает target() в отдельном потоке. Возвращает False, если за seconds он не завершился.\"\"\"\n",
    "    import sys\n",
    "    import threading\n",
    "\n",
    "    if sys.platform == \"emscripten\":  # Python в браузере: потоков нет, таймаут обеспечивает страница\n",
    "        target()\n",
    "        return True\n",
    "    # Большой стек нужен, чтобы глубокая рекурсия давала RecursionError, а не аварийное завершение Python.\n",
    "    for megabytes in (256, 64, 0):\n",
    "        try:\n",
    "            threading.stack_size(megabytes * 1024 * 1024)\n",
    "            worker = threading.Thread(target=target, daemon=True)\n",
    "            worker.start()\n",
    "        except (ValueError, RuntimeError, MemoryError):\n",
    "            continue\n",
    "        worker.join(seconds)\n",
    "        return not worker.is_alive()\n",
    "    target()  # потоки недоступны — запускаем без таймаута\n",
    "    return True\n",
    "\n",
    "\n",
    "def _test_info(fn) -> dict:\n",
    "    \"\"\"Описание теста, его лимит времени и сообщение на случай зависания.\"\"\"\n",
    "    limit = getattr(fn, \"timeout\", _TIMEOUT)\n",
    "    hint = getattr(fn, \"timeout_hint\", \"\")\n",
    "    message = (\n",
    "        f\"тест не завершился за {limit:g} с — возможно, бесконечный цикл или рекурсия: \"\n",
    "        \"проверьте, что границы/аргументы изменяются на каждом шаге\" + (f\". {hint}\" if hint else \"\")\n",
    "    )\n",
    "    return {\"title\": (fn.__doc__ or fn.__name__).strip(), \"timeout\": limit, \"timeout_message\": message}\n",
    "\n",
    "\n",
    "def _error_result(title: str, error: BaseException, user_file: str | None) -> dict:\n",
    "    \"\"\"Результат упавшего теста: статус, сообщение, тип исключения и строка в коде решения.\"\"\"\n",
    "    line = None\n",
    "    if user_file:  # самый глубокий кадр в коде решения; ошибка внутри самих тестов — без номера строки\n",
    "        tb = error.__traceback__\n",
    "        while tb is not None:\n",
    "            if tb.tb_frame.f_code.co_filename == user_file:\n",
    "                line = tb.tb_lineno\n",
    "            tb = tb.tb_next\n",
    "    status, message = \"error\", f\"{type(error).__name__}: {error}\"\n",
    "    if isinstance(error, NotImplementedError):\n",
    "        status, message, line = \"not_written\", \"функция ещё не написана\", None\n",
    "    elif isinstance(error, AssertionError):\n",
    "        status, message = \"failed\", str(error) or \"проверка не прошла\"\n",
    "    elif isinstance(error, RecursionError):\n",
    "        message = \"RecursionError: слишком глубокая рекурсия — проверьте базовый случай и то, что задача уменьшается\"\n",
    "    elif isinstance(error, MemoryError) or (isinstance(error, OverflowError) and \"index-sized\" in str(error)):\n",
    "        message = f\"{type(error).__name__}: не хватило памяти — возможно, создаётся слишком большая структура данных\"\n",
    "    return {\"title\": title, \"status\": status, \"message\": message, \"error_type\": type(error).__name__, \"line\": line}\n",
    "\n",
    "\n",
    "def _run_tests(verbose: bool = True, on_start=None, on_result=None, user_file: str | None = None) -> list[dict]:\n",
    "    \"\"\"Запускает тесты задачи (_TESTS — функции из tests.py) по порядку, каждый — с таймаутом.\n",
    "\n",
    "    Возвращает список результатов: {\"title\", \"status\", \"message\", \"error_type\", \"line\"}, где status —\n",
    "    passed · failed · error · not_written · timeout · not_run. Если тест завис, при verbose=True печатает\n",
    "    итог и завершает процесс: зависший поток остановить нельзя. on_start(i) и on_result(i, результат)\n",
    "    вызываются по мере выполнения; user_file — имя файла с кодом решения для номеров строк в ошибках.\n",
    "    \"\"\"\n",
    "    import os\n",
    "    import sys\n",
    "\n",
    "    if verbose:\n",
    "        try:  # ✓ и кириллица в консоли Windows со старой кодировкой\n",
    "            sys.stdout.reconfigure(encoding=\"utf-8\")\n",
    "        except Exception:  # noqa: BLE001 — reconfigure может не быть (перенаправленный вывод, браузер)\n",
    "            pass\n",
    "\n",
    "    tests = list(_TESTS)\n",
    "    results: list[dict] = []\n",
    "    not_written = hung = False\n",
    "    for index, fn in enumerate(tests):\n",
    "        info = _test_info(fn)\n",
    "        outcome: dict = {}\n",
    "\n",
    "        def target(fn=fn, outcome=outcome):\n",
    "            try:\n",
    "                fn()\n",
    "            except BaseException as e:\n",
    "                outcome[\"error\"] = e\n",
    "\n",
    "        if on_start:\n",
    "            on_start(index)\n",
    "        if not _call_with_timeout(target, info[\"timeout\"]):\n",
    "            results.append({\"title\": info[\"title\"], \"status\": \"timeout\", \"message\": info[\"timeout_message\"], \"error_type\": None, \"line\": None})\n",
    "            for other in tests[index + 1:]:\n",
    "                results.append({\"title\": _test_info(other)[\"title\"], \"status\": \"not_run\", \"message\": \"не запускался: предыдущий тест завис\", \"error_type\": None, \"line\": None})\n",
    "            hung = True\n",
    "            break\n",
    "        error = outcome.get(\"error\")\n",
    "        if error is None:\n",
    "            result = {\"title\": info[\"title\"], \"status\": \"passed\", \"message\": \"\", \"error_type\": None, \"line\": None}\n",
    "        else:\n",
    "            result = _error_result(info[\"title\"], error, user_file)\n",
    "            not_written = not_written or result[\"status\"] == \"not_written\"\n",
    "        results.append(result)\n",
    "        if on_result:\n",
    "            on_result(index, result)\n",
    "\n",
    "    if not_written:  # пока функция не написана, «даром» пройденные тесты не засчитываются\n",
    "        for r in results:\n",
    "            if r[\"status\"] == \"passed\":\n",
    "                r.update(status=\"not_written\", message=\"не засчитан, пока функция не написана\")\n",
    "    for r in results:\n",
    "        if len(r[\"message\"]) > 300:\n",
    "            r[\"message\"] = r[\"message\"][:300] + \"…\"\n",
    "\n",
    "    if verbose:\n",
    "        if not_written:\n",
    "            print(\"Функция ещё не написана — замените raise NotImplementedError своим кодом.\\n\")\n",
    "        for r in results:\n",
    "            mark = {\"passed\": \"✓\", \"not_run\": \"–\"}.get(r[\"status\"], \"✗\")\n",
    "            print(f\"{mark} {r['title']}\")\n",
    "            if r[\"message\"]:\n",
    "                print(f\"    {r['message']}\")\n",
    "        passed = sum(r[\"status\"] == \"passed\" for r in results)\n",
    "        print(\"─\" * 40)\n",
    "        print(f\"Прошло {passed} из {len(results)}\" + (\" — всё верно!\" if passed == len(results) else \"\"))\n",
    "        if hung:\n",
    "            print(\"Выполнение остановлено: один из тестов завис.\")\n",
    "            sys.stdout.flush()\n",
    "            os._exit(1)\n",
    "    return results\n",
    "\n",
    "\n",
    "def _check(*tests, names=()):\n",
    "    \"\"\"Запускает проверки упражнения и печатает ✓ / ✗ и итог — как на сайте.\"\"\"\n",
    "    for name in names:\n",
    "        if \".\" in name:  # «Класс.метод» из заготовки: класс должен быть объявлен\n",
    "            if not isinstance(globals().get(name.split(\".\")[0]), type):\n",
    "                print(f\"✗ класс {name.split('.')[0]} не объявлен — выполните ячейку упражнения\")\n",
    "                return\n",
    "            continue\n",
    "        if name not in globals():\n",
    "            print(f\"✗ переменная {name} не создана — выполните ячейку упражнения\")\n",
    "            return\n",
    "        if globals()[name] is Ellipsis:\n",
    "            print(f\"✗ {name} пока равна ... — замените многоточие своим кодом\")\n",
    "            return\n",
    "    globals()[\"_TESTS\"] = list(tests)\n",
    "    _run_tests()"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "setup"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ведомость — классическая таблица «строки × столбцы»: строка — студент, столбец — предмет. На ней сходится\n",
    "весь модуль: оси, `argmax`, статистика, сортировка. Файл `data/scores.csv`: первый столбец — код студента,\n",
    "дальше — баллы по пяти предметам."
   ],
   "id": "text-11"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Начало файла\n",
    "import numpy as np\n",
    "\n",
    "subjects = [\"математика\", \"физика\", \"программирование\", \"история\", \"английский\"]\n",
    "with open(\"data/scores.csv\") as f:\n",
    "    print(f.read()[:110])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "peek"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 1. Загрузка\n",
    "\n",
    "Баллы загрузим числами, а коды студентов — отдельно, как текст: у `loadtxt` для этого есть `dtype=str`."
   ],
   "id": "text-17"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 1. Баллы и коды\n",
    "\n",
    "Сохраните в `scores` таблицу баллов (столбцы 1–5), а в `ids` — коды студентов (столбец 0) как строки.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Баллы: `usecols=(1, 2, 3, 4, 5)`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Коды: `usecols=0` и `dtype=str`.\n",
    "\n",
    "</details>"
   ],
   "id": "load-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "scores = ...\n",
    "ids = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "load"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 1 — выполните после своего решения\n",
    "def test_scores():\n",
    "    \"scores — таблица 30 × 5\"\n",
    "    assert isinstance(scores, np.ndarray), f\"scores — это {type(scores).__name__}, а нужен массив из np.loadtxt\"\n",
    "    assert scores.shape == (30, 5), f\"форма scores — {scores.shape}, а нужно (30, 5): нужны пять столбцов с баллами\"\n",
    "    assert scores[0].tolist() == [68, 49, 68, 82, 81], \"первая строка должна быть баллами студента S01 по пяти предметам\"\n",
    "\n",
    "\n",
    "def test_ids():\n",
    "    \"ids — 30 кодов студентов\"\n",
    "    assert isinstance(ids, np.ndarray), f\"ids — это {type(ids).__name__}, а нужен массив строк из файла\"\n",
    "    assert ids.shape == (30,), f\"форма ids — {ids.shape}, а нужно 30 кодов\"\n",
    "    assert ids[0] == \"S01\" and ids[-1] == \"S30\", f\"коды должны идти от S01 до S30, а сейчас {ids[0]} … {ids[-1]}\"\n",
    "\n",
    "_check(test_scores, test_ids, names=[\"scores\",\"ids\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "load-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "scores = np.loadtxt(\"data/scores.csv\", delimiter=\",\", skiprows=1, usecols=(1, 2, 3, 4, 5))\n",
    "ids = np.loadtxt(\"data/scores.csv\", delimiter=\",\", skiprows=1, usecols=0, dtype=str)\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "load-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 2. Предметы"
   ],
   "id": "text-28"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 2. Самый трудный предмет\n",
    "\n",
    "Сохраните в `subject_avg` средний балл по каждому предмету, а в `hardest` — название предмета с самым низким\n",
    "средним (названия — в списке `subjects`).\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Среднее по столбцу — `axis=0`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "`subjects[subject_avg.argmin()]`.\n",
    "\n",
    "</details>"
   ],
   "id": "subjects-avg-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "subject_avg = ...\n",
    "hardest = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "subjects-avg"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 2 — выполните после своего решения\n",
    "def test_avg():\n",
    "    \"subject_avg — средний балл по каждому предмету\"\n",
    "    assert isinstance(subject_avg, np.ndarray), f\"subject_avg — это {type(subject_avg).__name__}, а нужен массив\"\n",
    "    assert subject_avg.shape != (30,), \"получилось 30 чисел — по студенту, а нужно по числу на предмет\"\n",
    "    assert np.allclose(subject_avg, [67.766667, 66.766667, 69.033333, 69.7, 65.966667]), f\"subject_avg = {np.round(subject_avg, 2).tolist()}\"\n",
    "\n",
    "\n",
    "def test_hardest():\n",
    "    \"hardest — предмет с самым низким средним\"\n",
    "    assert hardest != 4, \"4 — индекс; название возьмите из списка subjects\"\n",
    "    assert hardest != \"история\", \"это предмет с самым высоким средним; нужен с самым низким — argmin\"\n",
    "    assert hardest == \"английский\", f\"hardest = {hardest!r} — это не предмет с самым низким средним\"\n",
    "\n",
    "_check(test_avg, test_hardest, names=[\"subject_avg\",\"hardest\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "subjects-avg-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "subject_avg = scores.mean(axis=0)\n",
    "hardest = subjects[subject_avg.argmin()]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "subjects-avg-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 3. Студенты"
   ],
   "id": "text-38"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 3. Лучший студент\n",
    "\n",
    "Сохраните в `student_avg` средний балл каждого студента, а в `best_id` — код студента с самым высоким средним.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Среднее по строке — `axis=1`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Код лучшего — `ids[student_avg.argmax()]`.\n",
    "\n",
    "</details>"
   ],
   "id": "students-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "student_avg = ...\n",
    "best_id = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "students"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 3 — выполните после своего решения\n",
    "def test_avg():\n",
    "    \"student_avg — средний балл каждого студента\"\n",
    "    assert isinstance(student_avg, np.ndarray) and student_avg.shape == (30,), \"student_avg — 30 чисел, по студенту\"\n",
    "    assert abs(student_avg[0] - 69.6) < 1e-9, f\"средний балл S01 — {student_avg[0]}, а должен быть 69.6\"\n",
    "\n",
    "\n",
    "def test_best():\n",
    "    \"best_id — код лучшего студента\"\n",
    "    assert best_id != 11, \"11 — индекс; код возьмите из ids\"\n",
    "    assert best_id == \"S12\", f\"best_id = {best_id!r} — это не код студента с лучшим средним\"\n",
    "\n",
    "_check(test_avg, test_best, names=[\"student_avg\",\"best_id\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "students-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "student_avg = scores.mean(axis=1)\n",
    "best_id = ids[student_avg.argmax()]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "students-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 4. Лучший по каждому предмету\n",
    "\n",
    "Сохраните в `best_by_subject` коды студентов, лучших по каждому предмету, — пять кодов по порядку предметов.\n",
    "Если лучших несколько (одинаковый максимум), `argmax` берёт первого из них — так и оставьте.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`scores.argmax(axis=0)` — для каждого столбца индекс строки с максимумом.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Подставьте эти индексы в `ids[...]`.\n",
    "\n",
    "</details>"
   ],
   "id": "best-per-subject-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "best_by_subject = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "best-per-subject"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 4 — выполните после своего решения\n",
    "def test_best():\n",
    "    \"best_by_subject — лучший студент по каждому предмету\"\n",
    "    assert isinstance(best_by_subject, np.ndarray), f\"best_by_subject — это {type(best_by_subject).__name__}, а нужен массив кодов\"\n",
    "    got = best_by_subject.tolist()\n",
    "    assert got != [17, 28, 11, 8, 11], \"это индексы строк; по ним нужно взять коды из ids\"\n",
    "    assert len(got) == 5, f\"кодов {len(got)}, а предметов 5\"\n",
    "    assert got == [\"S18\", \"S29\", \"S12\", \"S09\", \"S12\"], f\"best_by_subject = {got}\"\n",
    "\n",
    "_check(test_best, names=[\"best_by_subject\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "best-per-subject-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "best_by_subject = ids[scores.argmax(axis=0)]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "best-per-subject-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 4. Кто сдал всё и кто в зоне риска\n",
    "\n",
    "`all` и `any` тоже понимают `axis`: `(scores >= 60).all(axis=1)` — для каждой строки, все ли её баллы не ниже 60."
   ],
   "id": "text-55"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 5. Сдали все предметы\n",
    "\n",
    "Сохраните в `all_passed` число студентов, у которых **все** баллы не ниже 60.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Маска «балл не ниже 60» — `scores >= 60`; «все в строке» — `.all(axis=1)`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Число подходящих студентов — сумма получившейся маски.\n",
    "\n",
    "</details>"
   ],
   "id": "passed-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "all_passed = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "passed"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 5 — выполните после своего решения\n",
    "def test_passed():\n",
    "    \"all_passed — студенты, у которых все баллы не ниже 60\"\n",
    "    assert not isinstance(all_passed, np.ndarray), \"all_passed — массив, а нужно одно число\"\n",
    "    assert all_passed != 5, \"5 — это число предметов, которые сдали все студенты, а нужно число студентов, сдавших все предметы\"\n",
    "    assert all_passed == 12, f\"all_passed = {all_passed} — это не число студентов, сдавших все предметы\"\n",
    "\n",
    "_check(test_passed, names=[\"all_passed\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "passed-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "all_passed = (scores >= 60).all(axis=1).sum()\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "passed-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 6. Зона риска\n",
    "\n",
    "Сохраните в `at_risk` коды студентов, у которых хотя бы по одному предмету меньше 50 баллов.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "«Хотя бы один в строке» — `.any(axis=1)`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Коды по маске — `ids[маска]`.\n",
    "\n",
    "</details>"
   ],
   "id": "risk-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "at_risk = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "risk"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 6 — выполните после своего решения\n",
    "def test_risk():\n",
    "    \"at_risk — коды студентов с баллом ниже 50\"\n",
    "    assert isinstance(at_risk, np.ndarray), f\"at_risk — это {type(at_risk).__name__}, а нужен массив кодов\"\n",
    "    assert at_risk.dtype != bool, \"это маска; по ней нужно отобрать коды из ids\"\n",
    "    assert len(at_risk) == 9, f\"в at_risk {len(at_risk)} кодов, а студентов с баллом ниже 50 — 9: вспомните any\"\n",
    "\n",
    "_check(test_risk, names=[\"at_risk\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "risk-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "at_risk = ids[(scores < 50).any(axis=1)]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "risk-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 5. Рейтинг"
   ],
   "id": "text-73"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 7. Пятёрка лучших\n",
    "\n",
    "Сохраните в `top5` коды пяти студентов с самым высоким средним баллом — от лучшего.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`np.argsort(student_avg)` — от худшего к лучшему; `[::-1]` — наоборот; `[:5]` — первые пять.\n",
    "\n",
    "</details>"
   ],
   "id": "top5-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "top5 = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "top5"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 7 — выполните после своего решения\n",
    "def test_top5():\n",
    "    \"top5 — пять лучших по среднему баллу\"\n",
    "    assert isinstance(top5, np.ndarray), f\"top5 — это {type(top5).__name__}, а нужен массив кодов\"\n",
    "    got = top5.tolist()\n",
    "    assert len(got) == 5, f\"в top5 {len(got)} кодов, а нужно 5\"\n",
    "    assert got != [\"S24\", \"S27\", \"S21\", \"S29\", \"S12\"], \"порядок перевёрнут: начните с лучшего\"\n",
    "    assert got != [\"S07\", \"S25\", \"S30\", \"S11\", \"S10\"], \"это пять худших: argsort упорядочивает по возрастанию\"\n",
    "    assert got == [\"S12\", \"S29\", \"S21\", \"S27\", \"S24\"], f\"top5 = {got}\"\n",
    "\n",
    "_check(test_top5, names=[\"top5\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "top5-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "top5 = ids[np.argsort(student_avg)[::-1][:5]]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "top5-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выводы"
   ],
   "id": "text-81"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Ведомость в цифрах\n",
    "for name, avg in zip(subjects, np.round(subject_avg, 1)):\n",
    "    print(f\"{name}: {avg}\")\n",
    "print(\"самый трудный:\", hardest, \"| лучший студент:\", best_id, round(student_avg.max(), 1))\n",
    "print(\"сдали всё:\", all_passed, \"из\", len(ids), \"| в зоне риска:\", len(at_risk))\n",
    "print(\"пятёрка лучших:\", top5)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "summary"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*После решения всех упражнений урока:*\n",
    "\n",
    "Самый трудный предмет — английский, но разница средних между предметами невелика — около 4 баллов. Все\n",
    "предметы сдали 12 студентов из 30, а у 9 есть хотя бы один балл ниже 50 — им стоит помочь. Весь анализ —\n",
    "десяток строк, и каждая — одна операция с таблицей."
   ],
   "id": "component-86"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Подробнее в справочнике:** [numpy/axis](https://ds-edu-trainer.vercel.app/reference/numpy/axis) · [numpy/sorting](https://ds-edu-trainer.vercel.app/reference/numpy/sorting) · [numpy/statistics](https://ds-edu-trainer.vercel.app/reference/numpy/statistics)"
   ],
   "id": "more"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
