{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Распределения и гистограммы\n",
    "\n",
    "Нормальное и равномерное распределения — рост людей и время ожидания; гистограмма np.histogram — сколько значений в каждом интервале; np.bincount — сколько раз каждое целое.\n",
    "\n",
    "Курс NumPy · урок `np-distributions` · [урок на сайте](https://ds-edu-trainer.vercel.app/courses/numpy/distributions).\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",
    "# ─── Проверка упражнений: тот же раннер, что у задач на сайте ───\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": [
    "`integers` и `random` выдают все значения одинаково часто. В жизни так бывает редко: рост людей чаще около\n",
    "среднего, чем 150 или 195 см. Как часто встречаются разные значения, описывает **распределение**\n",
    "(distribution).\n",
    "\n",
    "## Нормальное распределение\n",
    "\n",
    "Рост, вес, ошибки измерений похожи на **нормальное распределение**: большинство значений около среднего, а чем\n",
    "дальше от него, тем реже. Задаётся двумя числами: средним `loc` и стандартным отклонением `scale`:"
   ],
   "id": "text-9"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Рост тысячи человек\n",
    "import numpy as np\n",
    "\n",
    "rng = np.random.default_rng(1)\n",
    "heights = rng.normal(loc=170, scale=8, size=1000)\n",
    "print(np.round(heights[:6], 1))\n",
    "print(round(heights.mean(), 1), round(heights.std(), 1))"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "normal"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Среднее и отклонение получились близкими к заданным 170 и 8, но не точно — это случайная выборка.\n",
    "У нормального распределения примерно 68 % значений лежат в пределах одного отклонения от среднего:"
   ],
   "id": "text-20"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Доля в пределах одного отклонения\n",
    "print(((heights > 162) & (heights < 178)).mean())"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "within"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 1. Баллы экзамена\n",
    "\n",
    "Создайте генератор с seed 10 и смоделируйте баллы 500 участников экзамена: нормальное распределение со средним\n",
    "65 и отклонением 12. Сохраните их в `exam`, а в `high_share` — долю участников, набравших больше 80.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`rng.normal(loc=65, scale=12, size=500)`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Доля — среднее маски `exam > 80`.\n",
    "\n",
    "</details>"
   ],
   "id": "scores-sim-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "rng = ...\n",
    "exam = ...\n",
    "high_share = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "scores-sim"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 1 — выполните после своего решения\n",
    "def test_exam():\n",
    "    \"exam — 500 баллов со средним около 65 и отклонением около 12\"\n",
    "    assert isinstance(exam, np.ndarray) and exam.shape == (500,), \"exam — 500 значений\"\n",
    "    assert abs(exam.mean() - 65) < 2, f\"среднее exam — {exam.mean():.1f}, а должно быть около 65: проверьте параметры normal\"\n",
    "    assert abs(exam.std() - 12) < 2, f\"отклонение exam — {exam.std():.1f}, а должно быть около 12: проверьте параметры normal\"\n",
    "\n",
    "\n",
    "def test_share():\n",
    "    \"high_share — доля набравших больше 80\"\n",
    "    assert 0 < high_share < 1, f\"high_share = {high_share} — доля должна быть между 0 и 1: среднее маски\"\n",
    "    assert abs(exam[0] - 51.759939) < 1e-5, \"exam получен не тем генератором: создайте его с seed 10 прямо перед normal\"\n",
    "    assert abs(high_share - 0.084) < 1e-9, f\"high_share = {high_share} — это не доля набравших больше 80\"\n",
    "\n",
    "_check(test_exam, test_share, names=[\"rng\",\"exam\",\"high_share\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "scores-sim-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "rng = np.random.default_rng(10)\n",
    "exam = rng.normal(loc=65, scale=12, size=500)\n",
    "high_share = (exam > 80).mean()\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "scores-sim-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Равномерное распределение\n",
    "\n",
    "`rng.uniform(a, b, size=n)` — дробные числа, которые с равной вероятностью попадают куда угодно между `a` и `b`.\n",
    "Так можно моделировать, например, время ожидания автобуса, который ходит раз в 10 минут:"
   ],
   "id": "text-33"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Время ожидания автобуса\n",
    "rng = np.random.default_rng(2)\n",
    "waits_demo = rng.uniform(0, 10, size=5)\n",
    "print(np.round(waits_demo, 2))"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "uniform"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 2. Долгое ожидание\n",
    "\n",
    "Создайте генератор с seed 8 и смоделируйте 500 ожиданий автобуса — равномерно от 0 до 10 минут. Сохраните\n",
    "их в `waits`, а в `long_share` — долю ожиданий дольше 7 минут.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`rng.uniform(0, 10, size=500)`.\n",
    "\n",
    "</details>"
   ],
   "id": "bus-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "rng = ...\n",
    "waits = ...\n",
    "long_share = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "bus"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 2 — выполните после своего решения\n",
    "def test_waits():\n",
    "    \"waits — 500 ожиданий от 0 до 10 минут\"\n",
    "    assert isinstance(waits, np.ndarray) and waits.shape == (500,), \"waits — 500 значений\"\n",
    "    assert waits.min() >= 0 and waits.max() <= 10, f\"значения от {waits.min():.2f} до {waits.max():.2f}, а должны быть от 0 до 10\"\n",
    "\n",
    "\n",
    "def test_share():\n",
    "    \"long_share — доля ожиданий дольше 7 минут\"\n",
    "    assert abs(waits.mean() - 4.948001) < 1e-5, \"waits получены не тем генератором: seed 8, прямо перед uniform\"\n",
    "    assert abs(long_share - 0.292) < 1e-9, f\"long_share = {long_share} — это не доля ожиданий дольше 7 минут\"\n",
    "\n",
    "_check(test_waits, test_share, names=[\"rng\",\"waits\",\"long_share\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "bus-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "rng = np.random.default_rng(8)\n",
    "waits = rng.uniform(0, 10, size=500)\n",
    "long_share = (waits > 7).mean()\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "bus-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Гистограмма: np.histogram\n",
    "\n",
    "Чтобы увидеть распределение, значения раскладывают по интервалам и считают, сколько попало в каждый. Это\n",
    "**гистограмма** (histogram). `np.histogram(a, bins=…)` возвращает два массива: сколько значений в каждом\n",
    "интервале и сами границы интервалов. В `bins` можно передать список границ:"
   ],
   "id": "text-47"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Рост по интервалам в 10 см\n",
    "counts_demo, edges_demo = np.histogram(heights, bins=[140, 150, 160, 170, 180, 190, 200, 210])\n",
    "print(counts_demo)\n",
    "print(edges_demo)\n",
    "print(counts_demo.sum())"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "histogram"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Интервал включает левую границу и не включает правую — `[150, 160)`: рост ровно 160 см попадёт в следующий.\n",
    "Исключение — последний интервал, у него включены обе границы. Можно передать и число — сколько интервалов\n",
    "равной ширины сделать между минимумом и максимумом."
   ],
   "id": "text-55"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 3. Баллы по интервалам\n",
    "\n",
    "Разложите баллы `exam` по интервалам 0–40, 40–60, 60–80, 80–100 и сохраните в `counts` число участников\n",
    "в каждом (четыре числа).\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`np.histogram(exam, bins=[0, 40, 60, 80, 100])`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Функция возвращает два массива; первый — число значений: `counts, edges = np.histogram(...)`.\n",
    "\n",
    "</details>"
   ],
   "id": "exam-hist-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "counts = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "exam-hist"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 3 — выполните после своего решения\n",
    "def test_counts():\n",
    "    \"counts — участники в четырёх интервалах\"\n",
    "    assert not isinstance(counts, tuple), \"np.histogram вернула два массива: разложите результат на две переменные\"\n",
    "    assert isinstance(counts, np.ndarray), f\"counts — это {type(counts).__name__}, а нужен массив количеств\"\n",
    "    assert len(counts) == 4, f\"в counts {len(counts)} чисел, а интервалов 4 — границ должно быть 5\"\n",
    "    assert counts.tolist() == [11, 171, 276, 42], f\"counts = {counts.tolist()}: проверьте границы интервалов 0–40, 40–60, 60–80, 80–100\"\n",
    "\n",
    "_check(test_counts, names=[\"counts\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "exam-hist-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "counts, edges = np.histogram(exam, bins=[0, 40, 60, 80, 100])\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "exam-hist-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Вопрос.** В `bins` передали 5 границ: `[0, 25, 50, 75, 100]`. Сколько чисел будет в массиве с количествами?\n",
    "\n",
    "- 5\n",
    "- 4\n",
    "- 100\n",
    "\n",
    "<details><summary>Ответ</summary>\n",
    "\n",
    "4\n",
    "\n",
    "5 границ делят отрезок на 4 интервала: 0–25, 25–50, 50–75, 75–100. Количеств столько же, сколько интервалов.\n",
    "\n",
    "</details>"
   ],
   "id": "quiz-68"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Сколько раз каждое целое: np.bincount\n",
    "\n",
    "Для целых чисел есть способ проще: `np.bincount(a)` возвращает массив, где на месте `k` стоит, сколько раз\n",
    "в `a` встретилось число `k`. Удобно для кубиков — индекс массива и есть выпавшее число:"
   ],
   "id": "text-77"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# 600 бросков кубика\n",
    "rolls_demo = np.random.default_rng(42).integers(1, 7, size=600)\n",
    "print(np.bincount(rolls_demo))\n",
    "print(np.bincount(rolls_demo)[1:])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "bincount"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На месте 0 стоит 0: ноль на кубике не выпадает. Чтобы оставить только грани 1–6, берут срез `[1:]`."
   ],
   "id": "text-84"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 4. Какая грань чаще\n",
    "\n",
    "Создайте генератор с seed 4, сделайте 1200 бросков кубика и сохраните в `faces` число выпадений каждой грани\n",
    "от 1 до 6 (шесть чисел), а в `top_face` — грань, выпавшую чаще всех.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`np.bincount(rolls)[1:]` — без нулевой грани.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Грань — индекс максимума плюс один (после среза нулевая грань исчезла).\n",
    "\n",
    "</details>"
   ],
   "id": "faces-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "rolls = ...\n",
    "faces = ...\n",
    "top_face = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "faces"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 4 — выполните после своего решения\n",
    "def test_faces():\n",
    "    \"faces — выпадения граней 1–6\"\n",
    "    assert isinstance(faces, np.ndarray), f\"faces — это {type(faces).__name__}, а нужен массив из np.bincount\"\n",
    "    assert len(faces) != 7, \"в faces 7 чисел: уберите нулевую грань\"\n",
    "    assert len(faces) == 6, f\"в faces {len(faces)} чисел, а граней 6\"\n",
    "    assert faces.sum() == 1200, f\"сумма faces — {faces.sum()}, а бросков 1200\"\n",
    "\n",
    "\n",
    "def test_top():\n",
    "    \"top_face — самая частая грань\"\n",
    "    assert faces.tolist() == [197, 197, 187, 190, 210, 219], \"броски получены не тем генератором: seed 4, 1200 бросков\"\n",
    "    assert top_face != 5, \"5 — индекс в faces, а грани нумеруются с 1\"\n",
    "    assert top_face == 6, f\"top_face = {top_face} — это не самая частая грань\"\n",
    "\n",
    "_check(test_faces, test_top, names=[\"rolls\",\"faces\",\"top_face\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "faces-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "rolls = np.random.default_rng(4).integers(1, 7, size=1200)\n",
    "faces = np.bincount(rolls)[1:]\n",
    "top_face = faces.argmax() + 1\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "faces-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Итоги\n",
    "\n",
    "- `rng.normal(loc, scale, size)` — нормальное распределение: значения теснятся около среднего.\n",
    "- `rng.uniform(a, b, size)` — равномерно между `a` и `b`.\n",
    "- `counts, edges = np.histogram(a, bins=…)` — число значений в каждом интервале и границы.\n",
    "- `np.bincount(a)` — сколько раз встретилось каждое целое от 0 до максимума; работает только с целыми\n",
    "  неотрицательными числами."
   ],
   "id": "text-94"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Подробнее в справочнике:** [numpy/random](https://ds-edu-trainer.vercel.app/reference/numpy/random) · [numpy/histogram](https://ds-edu-trainer.vercel.app/reference/numpy/histogram)"
   ],
   "id": "more"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
