{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Сортировка и уникальные\n",
    "\n",
    "Отсортировать значения, получить порядок индексов argsort — и с его помощью расставить по местам имена; посчитать, какие значения встречаются и сколько раз.\n",
    "\n",
    "Курс NumPy · урок `np-sorting` · [урок на сайте](https://ds-edu-trainer.vercel.app/courses/numpy/sorting).\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": [
    "Кто в пятёрке лучших? Какие значения встречаются в данных и сколько раз каждое? Для рейтингов, топов и подсчёта\n",
    "частот нужны сортировка и поиск уникальных значений — о них этот урок.\n",
    "\n",
    "## Отсортировать: np.sort\n",
    "\n",
    "`np.sort(a)` возвращает **новый** отсортированный массив, по возрастанию. Чтобы получить убывание, результат\n",
    "разворачивают срезом `[::-1]`:"
   ],
   "id": "text-9"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Цены по возрастанию и по убыванию\n",
    "import numpy as np\n",
    "\n",
    "prices = np.array([320, 150, 890, 45, 610, 210])\n",
    "print(np.sort(prices))\n",
    "print(np.sort(prices)[::-1])\n",
    "print(prices)    # исходный порядок сохранился"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "sort"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "У массива есть и метод `sort()`, но он работает иначе: сортирует **сам массив** на месте и ничего не\n",
    "возвращает."
   ],
   "id": "text-19"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Метод sort() меняет массив\n",
    "nums = np.array([3, 1, 2])\n",
    "result = nums.sort()\n",
    "print(result)\n",
    "print(nums)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "sort-method"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Типичная ошибка: x = a.sort().**\n",
    ">\n",
    "> Метод `sort()` возвращает `None` — «ничего». Запись `x = a.sort()` отсортирует `a`, а в `x` окажется `None`.\n",
    "> Нужен отсортированный результат в новой переменной — пишите `x = np.sort(a)`."
   ],
   "id": "component-25"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 1. Три самые дорогие покупки\n",
    "\n",
    "Сохраните в `top3` три самые большие суммы из `checks`, от большей к меньшей. Сам `checks` не меняйте.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`np.sort(checks)` — по возрастанию; развернуть — `[::-1]`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Первые три элемента развёрнутого массива — срез `[:3]`.\n",
    "\n",
    "</details>"
   ],
   "id": "top3-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "checks = np.array([1450, 380, 2990, 760, 2210, 540, 3120, 890])\n",
    "top3 = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "top3"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 1 — выполните после своего решения\n",
    "def test_top3():\n",
    "    \"top3 — три самые большие суммы по убыванию\"\n",
    "    assert top3 is not None, \"top3 = None: метод sort() ничего не возвращает — используйте функцию np.sort\"\n",
    "    assert isinstance(top3, np.ndarray), f\"top3 — это {type(top3).__name__}, а нужен массив\"\n",
    "    got = top3.tolist()\n",
    "    assert got != [380, 540, 760], \"это три самые маленькие суммы, а нужны самые большие\"\n",
    "    assert got != [2210, 2990, 3120], \"суммы верные, но по возрастанию; нужно от большей к меньшей\"\n",
    "    assert got == [3120, 2990, 2210], f\"top3 = {got} — это не три самые большие суммы\"\n",
    "\n",
    "\n",
    "def test_checks():\n",
    "    \"checks не изменился\"\n",
    "    assert checks.tolist() == [1450, 380, 2990, 760, 2210, 540, 3120, 890], \"checks изменился: метод sort() меняет сам массив, а функция np.sort возвращает копию\"\n",
    "\n",
    "_check(test_top3, test_checks, names=[\"top3\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "top3-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "checks = np.array([1450, 380, 2990, 760, 2210, 540, 3120, 890])\n",
    "top3 = np.sort(checks)[::-1][:3]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "top3-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Порядок индексов: np.argsort\n",
    "\n",
    "Часто сортировать нужно не сами числа, а **то, что с ними связано**: имена студентов по их баллам. Для этого\n",
    "есть `np.argsort` — он возвращает не значения, а **индексы** в том порядке, в каком значения шли бы после\n",
    "сортировки:"
   ],
   "id": "text-36"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# argsort\n",
    "points = np.array([78, 92, 85, 64, 88])\n",
    "print(np.argsort(points))\n",
    "print(points[np.argsort(points)])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "argsort"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Массив индексов можно подставить в квадратные скобки — получится массив элементов в этом порядке. Так один\n",
    "и тот же порядок применяется к любому массиву той же длины:"
   ],
   "id": "text-44"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Рейтинг студентов\n",
    "names = np.array([\"Анна\", \"Иван\", \"Мария\", \"Олег\", \"Елена\"])\n",
    "order = np.argsort(points)[::-1]    # от большего балла к меньшему\n",
    "print(order)\n",
    "print(names[order])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "rank"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`names[order]` — выборка по **массиву индексов**: элементы берутся в том порядке, в каком перечислены индексы.\n",
    "Как и выборка по маске, это копия."
   ],
   "id": "text-49"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 2. Рейтинг продавцов\n",
    "\n",
    "В `sellers` — имена продавцов, в `revenue` — их выручка за месяц. Сохраните в `ranking` имена продавцов от\n",
    "самой большой выручки к самой маленькой.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`np.argsort(revenue)` — индексы от меньшей выручки к большей; разверните `[::-1]`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Подставьте получившиеся индексы в `sellers[...]`.\n",
    "\n",
    "</details>"
   ],
   "id": "leaders-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "sellers = np.array([\"Пётр\", \"Юлия\", \"Максим\", \"Светлана\", \"Роман\"])\n",
    "revenue = np.array([410000, 530000, 290000, 610000, 380000])\n",
    "ranking = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "leaders"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 2 — выполните после своего решения\n",
    "def test_ranking():\n",
    "    \"ranking — имена от большей выручки к меньшей\"\n",
    "    assert isinstance(ranking, np.ndarray), f\"ranking — это {type(ranking).__name__}, а нужен массив имён\"\n",
    "    got = ranking.tolist()\n",
    "    assert got != [3, 1, 0, 4, 2], \"это индексы; по ним нужно взять имена из sellers\"\n",
    "    assert got != [\"Максим\", \"Роман\", \"Пётр\", \"Юлия\", \"Светлана\"], \"порядок от меньшей выручки, а нужно от большей\"\n",
    "    assert got != [\"Юлия\", \"Светлана\", \"Роман\", \"Пётр\", \"Максим\"], \"имена отсортированы по алфавиту, а нужно по выручке\"\n",
    "    assert got == [\"Светлана\", \"Юлия\", \"Пётр\", \"Роман\", \"Максим\"], f\"ranking = {got}\"\n",
    "\n",
    "_check(test_ranking, names=[\"ranking\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "leaders-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "sellers = np.array([\"Пётр\", \"Юлия\", \"Максим\", \"Светлана\", \"Роман\"])\n",
    "revenue = np.array([410000, 530000, 290000, 610000, 380000])\n",
    "ranking = sellers[np.argsort(revenue)[::-1]]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "leaders-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Вопрос.** Что получится: `np.argsort(np.array([30, 10, 20]))`?\n",
    "\n",
    "- `array([10, 20, 30])`\n",
    "- `array([1, 2, 0])`\n",
    "- `array([2, 0, 1])`\n",
    "\n",
    "<details><summary>Ответ</summary>\n",
    "\n",
    "`array([1, 2, 0])`\n",
    "\n",
    "Самое маленькое значение (10) стоит под индексом 1, следующее (20) — под индексом 2, самое большое (30) — под индексом 0. `argsort` возвращает индексы, а не значения.\n",
    "\n",
    "</details>"
   ],
   "id": "quiz-61"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Уникальные значения: np.unique\n",
    "\n",
    "`np.unique(a)` возвращает отсортированный массив **разных** значений. С `return_counts=True` — ещё и сколько раз\n",
    "встречается каждое. Результат тогда — два массива, их удобно сразу разложить по двум именам:"
   ],
   "id": "text-70"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Какие оценки и сколько раз\n",
    "marks_demo = np.array([5, 4, 4, 3, 5, 5, 2, 4, 3, 5, 4, 4])\n",
    "print(np.unique(marks_demo))\n",
    "values_demo, counts_demo = np.unique(marks_demo, return_counts=True)\n",
    "print(values_demo)\n",
    "print(counts_demo)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "unique"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 3. Распределение оценок\n",
    "\n",
    "В `marks` — оценки за контрольную. Сохраните в `values` разные оценки по возрастанию, в `counts` — сколько раз\n",
    "встречается каждая, а в `most_common` — самую частую оценку.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`values, counts = np.unique(marks, return_counts=True)`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Самая частая — та, где `counts` наибольшее: `values[counts.argmax()]`.\n",
    "\n",
    "</details>"
   ],
   "id": "grades-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "marks = np.array([4, 3, 5, 4, 4, 2, 5, 3, 4, 3, 3, 5, 3, 4, 3, 3])\n",
    "values = ...\n",
    "counts = ...\n",
    "most_common = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "grades"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 3 — выполните после своего решения\n",
    "def test_values():\n",
    "    \"values — разные оценки по возрастанию\"\n",
    "    assert isinstance(values, np.ndarray) and values.tolist() == [2, 3, 4, 5], \"values — это должны быть разные оценки из marks по возрастанию\"\n",
    "\n",
    "\n",
    "def test_counts():\n",
    "    \"counts — сколько раз каждая\"\n",
    "    assert isinstance(counts, np.ndarray) and counts.tolist() == [1, 7, 5, 3], \"counts — это не число повторов каждой оценки: вспомните параметр return_counts\"\n",
    "\n",
    "\n",
    "def test_most():\n",
    "    \"most_common — самая частая оценка\"\n",
    "    assert most_common != 7, \"7 — сколько раз встречается самая частая оценка, а нужна сама оценка\"\n",
    "    assert most_common != 1, \"1 — это индекс; оценку возьмите из values по этому индексу\"\n",
    "    assert most_common == 3, f\"most_common = {most_common} — это не самая частая оценка\"\n",
    "\n",
    "_check(test_values, test_counts, test_most, names=[\"values\",\"counts\",\"most_common\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "grades-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "marks = np.array([4, 3, 5, 4, 4, 2, 5, 3, 4, 3, 3, 5, 3, 4, 3, 3])\n",
    "values, counts = np.unique(marks, return_counts=True)\n",
    "most_common = values[counts.argmax()]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "grades-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Итоги\n",
    "\n",
    "- `np.sort(a)` — новый отсортированный массив; `[::-1]` — по убыванию.\n",
    "- `a.sort()` сортирует сам `a` и возвращает `None`.\n",
    "- `np.argsort(a)` — индексы в порядке сортировки; `b[np.argsort(a)]` упорядочивает `b` по значениям `a`.\n",
    "- `a[массив_индексов]` — элементы в указанном порядке (копия).\n",
    "- `np.unique(a, return_counts=True)` — разные значения и сколько раз каждое встречается."
   ],
   "id": "text-85"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Подробнее в справочнике:** [numpy/sorting](https://ds-edu-trainer.vercel.app/reference/numpy/sorting) · [numpy/unique-sets](https://ds-edu-trainer.vercel.app/reference/numpy/unique-sets)"
   ],
   "id": "more"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
