{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Столбец и строка\n",
    "\n",
    "Одномерный массив всегда прикладывается как строка. Чтобы приложить его как столбец, нужна форма (n, 1) — через np.newaxis или keepdims=True. Таблица всех пар и ловушка квадратной матрицы.\n",
    "\n",
    "Курс NumPy · урок `np-columns` · [урок на сайте](https://ds-edu-trainer.vercel.app/courses/numpy/columns).\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": [
    "В прошлом уроке наценка магазинов — массив из трёх чисел — не приложилась к таблице 3 × 4. Одномерный массив\n",
    "транслируется как **строка**, а нам нужно, чтобы каждое число приложилось к своей строке таблицы, то есть\n",
    "массив вёл себя как **столбец**.\n",
    "\n",
    "## Форма (n, 1)\n",
    "\n",
    "Столбец — это двумерный массив формы `(3, 1)`: три строки, один столбец. По правилам транслирования `(3, 1)` и\n",
    "`(3, 4)` совместимы: 3 = 3, а 1 растягивается до 4. Превратить `(3,)` в `(3, 1)` можно, добавив ось длины 1:\n",
    "`markup[:, np.newaxis]` — «все элементы по оси 0, и новая ось». (Тот же столбец даёт знакомый по модулю 3\n",
    "`markup.reshape(-1, 1)`.)"
   ],
   "id": "text-10"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Наценка магазинов — столбцом\n",
    "import numpy as np\n",
    "\n",
    "prices = np.array([\n",
    "    [120, 80, 45, 300],\n",
    "    [130, 85, 50, 290],\n",
    "    [110, 90, 40, 310],\n",
    "])\n",
    "markup = np.array([1.1, 1.2, 1.05])\n",
    "print(markup.shape, markup[:, np.newaxis].shape)\n",
    "prices * markup[:, np.newaxis]"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "newaxis"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 1. Своя наценка у каждого магазина\n",
    "\n",
    "В `store_markup` — наценка каждого из трёх магазинов (множитель). Сохраните в `final` таблицу цен `prices`\n",
    "с наценкой: каждая строка умножается на наценку своего магазина.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Наценка должна вести себя как столбец: `store_markup[:, np.newaxis]`.\n",
    "\n",
    "</details>"
   ],
   "id": "store-markup-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "store_markup = np.array([1.25, 1.0, 1.1])\n",
    "final = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "store-markup"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 1 — выполните после своего решения\n",
    "def test_shape():\n",
    "    \"final — таблица 3 × 4\"\n",
    "    assert isinstance(final, np.ndarray), f\"final — это {type(final).__name__}, а нужен массив\"\n",
    "    assert final.shape == (3, 4), f\"форма final — {final.shape}, а должна остаться (3, 4)\"\n",
    "\n",
    "\n",
    "def test_values():\n",
    "    \"каждая строка умножена на наценку своего магазина\"\n",
    "    assert isinstance(final, np.ndarray) and final.shape == (3, 4), \"сначала исправьте то, о чём говорит проверка выше\"\n",
    "    assert np.allclose(final, [[150, 100, 56.25, 375], [130, 85, 50, 290], [121, 99, 44, 341]]), f\"final = {final.tolist()}: каждая строка (магазин) умножается на свою наценку\"\n",
    "\n",
    "_check(test_shape, test_values, names=[\"final\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "store-markup-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "store_markup = np.array([1.25, 1.0, 1.1])\n",
    "final = prices * store_markup[:, np.newaxis]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "store-markup-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## keepdims=True\n",
    "\n",
    "Часто столбец получается из агрегата: сумма каждой строки — `sum(axis=1)` — даёт форму `(3,)`, и её снова\n",
    "пришлось бы превращать в столбец. Параметр `keepdims=True` сохраняет свёрнутую ось длиной 1 — результат сразу\n",
    "форма `(3, 1)`:"
   ],
   "id": "text-30"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Доля месяца в годовой выручке\n",
    "monthly = np.array([            # строка — магазин, столбец — месяц\n",
    "    [40, 42, 45, 50, 48, 52, 55, 58, 50, 47, 60, 75],\n",
    "    [30, 28, 33, 35, 40, 38, 36, 35, 39, 58, 45, 55],\n",
    "    [70, 72, 68, 75, 80, 85, 90, 125, 82, 79, 95, 120],\n",
    "])\n",
    "year = monthly.sum(axis=1, keepdims=True)\n",
    "print(year.shape)\n",
    "shares = monthly / year\n",
    "print(shares.sum(axis=1))\n",
    "print(np.round(shares[:, -1] * 100, 1))   # доля декабря, %"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "keepdims"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Каждая строка разделилась на сумму **своей** строки — у каждого магазина доли месяцев в сумме дают 1."
   ],
   "id": "text-38"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 2. Сильные предметы студента\n",
    "\n",
    "Сохраните в `personal` таблицу: насколько каждый балл студента выше или ниже **его собственного** среднего\n",
    "балла (форма 30 × 5).\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Среднее каждого студента — `scores.mean(axis=1, keepdims=True)`, форма `(30, 1)`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Вычтите этот столбец из таблицы.\n",
    "\n",
    "</details>"
   ],
   "id": "personal-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "scores = np.loadtxt(\"data/scores.csv\", delimiter=\",\", skiprows=1, usecols=(1, 2, 3, 4, 5))\n",
    "personal = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "personal"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 2 — выполните после своего решения\n",
    "def test_shape():\n",
    "    \"personal — таблица 30 × 5\"\n",
    "    assert isinstance(personal, np.ndarray), f\"personal — это {type(personal).__name__}, а нужна таблица\"\n",
    "    assert personal.shape == (30, 5), f\"форма personal — {personal.shape}, а должна быть (30, 5)\"\n",
    "\n",
    "\n",
    "def test_rows():\n",
    "    \"в каждой строке среднее отклонение — 0\"\n",
    "    assert isinstance(personal, np.ndarray) and personal.shape == (30, 5), \"сначала исправьте то, о чём говорит проверка выше\"\n",
    "    assert not np.allclose(personal.mean(axis=0), 0), \"вычтено среднее по предметам, а нужно среднее каждого студента\"\n",
    "    assert np.allclose(personal.mean(axis=1), 0), \"в каждой строке среднее должно стать 0: вычитайте среднее своей строки\"\n",
    "    assert np.allclose(personal[0], [-1.6, -20.6, -1.6, 12.4, 11.4]), f\"у первого студента получилось {np.round(personal[0], 1).tolist()}\"\n",
    "\n",
    "_check(test_shape, test_rows, names=[\"personal\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "personal-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",
    "personal = scores - scores.mean(axis=1, keepdims=True)\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "personal-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Вопрос.** Какая форма у `np.arange(3)[:, np.newaxis] * np.arange(4)`?\n",
    "\n",
    "- `(3,)`\n",
    "- `(4, 3)`\n",
    "- `(3, 4)`\n",
    "\n",
    "<details><summary>Ответ</summary>\n",
    "\n",
    "`(3, 4)`\n",
    "\n",
    "`(3, 1)` и `(4,)` → `(1, 4)`; растягиваются оба: у `(1, 4)` единица растягивается до 3, у `(3, 1)` — до 4. Получается таблица 3 × 4 — все пары.\n",
    "\n",
    "</details>"
   ],
   "id": "quiz-49"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Таблица всех пар\n",
    "\n",
    "Если столбец `(n, 1)` встречается со строкой `(m,)`, растягиваются **оба**, и получается таблица `n × m` всех\n",
    "пар — например, таблица умножения:"
   ],
   "id": "text-58"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Таблица умножения\n",
    "n = np.arange(1, 10)\n",
    "n[:, np.newaxis] * n"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "outer"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 3. Цена за количество\n",
    "\n",
    "В `unit_price` — цены пяти товаров, в `amounts` — варианты количества (1, 2, 5, 10). Сохраните в `cost` таблицу\n",
    "стоимости: строка — товар, столбец — количество.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Товары — в строках, значит, цены — столбцом: `unit_price[:, np.newaxis]`.\n",
    "\n",
    "</details>"
   ],
   "id": "combos-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "unit_price = np.array([120, 80, 45, 300, 15])\n",
    "amounts = np.array([1, 2, 5, 10])\n",
    "cost = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "combos"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 3 — выполните после своего решения\n",
    "def test_cost():\n",
    "    \"cost — таблица 5 товаров × 4 количества\"\n",
    "    assert isinstance(cost, np.ndarray), f\"cost — это {type(cost).__name__}, а нужна таблица\"\n",
    "    assert cost.shape != (4, 5), \"товары оказались в столбцах: цены должны быть столбцом\"\n",
    "    assert cost.shape == (5, 4), f\"форма cost — {cost.shape}, а нужно (5, 4)\"\n",
    "    assert cost[3].tolist() == [300, 600, 1500, 3000], f\"строка товара за 300 ₽ — {cost[3].tolist()}: в строке — цена 1, 2, 5 и 10 штук\"\n",
    "\n",
    "_check(test_cost, names=[\"cost\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "combos-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "unit_price = np.array([120, 80, 45, 300, 15])\n",
    "amounts = np.array([1, 2, 5, 10])\n",
    "cost = unit_price[:, np.newaxis] * amounts\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "combos-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ловушка квадратной таблицы"
   ],
   "id": "text-72"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Квадратная таблица: ошибки нет, а ответ неверный\n",
    "sq = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n",
    "print(sq.mean(axis=1))\n",
    "print(sq - sq.mean(axis=1))                  # неверно: вычли как строку\n",
    "print(sq - sq.mean(axis=1, keepdims=True))   # верно: каждая строка минус своё среднее"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "square"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Типичная ошибка: (n,) вместо (n, 1) у квадратной таблицы.**\n",
    ">\n",
    "> Если таблица квадратная, `(3, 3) - (3,)` проходит без ошибки: формы совместимы, массив приложился как строка.\n",
    "> Но хотели-то вычесть среднее **каждой строки** — а вычли по столбцам. Ошибки нет, результат неверный. Поэтому\n",
    "> у агрегатов по строкам почти всегда пишут `keepdims=True`."
   ],
   "id": "component-77"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 4. Центрировать строки\n",
    "\n",
    "Таблица `sq` — квадратная. Вычтите из каждой строки её собственное среднее и сохраните результат в `centered`:\n",
    "в каждой строке среднее должно стать 0.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Среднее строк — `axis=1`; чтобы оно приложилось как столбец, добавьте `keepdims=True`.\n",
    "\n",
    "</details>"
   ],
   "id": "row-center-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "centered = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "row-center"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 4 — выполните после своего решения\n",
    "def test_centered():\n",
    "    \"в каждой строке среднее стало 0\"\n",
    "    assert isinstance(centered, np.ndarray) and centered.shape == (3, 3), \"centered — таблица 3 × 3\"\n",
    "    assert centered.tolist() != [[-1, -3, -5], [2, 0, -2], [5, 3, 1]], \"среднее приложилось как строка: нужно сохранить у него форму столбца\"\n",
    "    assert centered.tolist() == [[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]], f\"centered = {centered.tolist()}\"\n",
    "\n",
    "_check(test_centered, names=[\"centered\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "row-center-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "centered = sq - sq.mean(axis=1, keepdims=True)\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "row-center-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Итоги\n",
    "\n",
    "- Одномерный массив `(n,)` транслируется как строка; столбец — это форма `(n, 1)`.\n",
    "- `a[:, np.newaxis]` добавляет ось: `(n,)` → `(n, 1)`.\n",
    "- `keepdims=True` у агрегата сохраняет свёрнутую ось — результат сразу приложится к своим строкам.\n",
    "- Столбец `(n, 1)` и строка `(m,)` дают таблицу всех пар `n × m`.\n",
    "- У квадратной таблицы ошибки не будет — проверяйте, что прикладываете столбец, а не строку."
   ],
   "id": "text-89"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Подробнее в справочнике:** [numpy/broadcasting](https://ds-edu-trainer.vercel.app/reference/numpy/broadcasting) · [numpy/transpose](https://ds-edu-trainer.vercel.app/reference/numpy/transpose)"
   ],
   "id": "more"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
