{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Склеить и разделить\n",
    "\n",
    "Собрать массив из частей — np.concatenate по существующей оси, np.stack с новой осью, np.vstack и np.hstack — и разрезать его на части np.split.\n",
    "\n",
    "Курс NumPy · урок `np-join-split` · [урок на сайте](https://ds-edu-trainer.vercel.app/courses/numpy/join-split).\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",
    "    \"station_center.csv\": \"\"\"\n",
    "eNotkEFqRDEMQ/c+i2Jix4mT4xR+oX8xpYspZW5flMkqPCTLcq6PF56fjx98/T7u636+8Hd/X2IoXRumw3SJH4pNjZTIhGtIHGow\n",
    "DelABrouGShNO9K3K1FCG2eGhkzOdEyDa8oiBXLtBKsoqcbAqk3MmDOYOnWKOXESqy6xdtS+t9gu4xgNTVOso/g7qmkVG6dERxDz\n",
    "zOa+yibNwZODixYqS50PqKjqSD5T3N5EbYg7ivGaCqfYUKoOjA5TFw+q2xxq4p2qsQP7+4DTm+j0JsVF79AmPlG1YvZ9zD89y1E+\n",
    "\"\"\",\n",
    "    \"station_airport.csv\": \"\"\"\n",
    "eNo1kEFOBDEMBO9+S8eK7ThxnoM0SMxhEYdFaH+PegaO5SSVbh9vLzzfH1/4+H6cx/l84ef8PMTQpg5UwTXFSYlKDE0JUqAmproM\n",
    "tFLDWggdkjeVITRkoqUWypE6Zf1RR2qXoqVQC6kmG22R+M7FOnFibQwdYgY6QruYg1PnNPBvtkHZxkq4LrFECw1mStom2tB1lwmx\n",
    "RZxs43xbjGX3ByG2gVVXQu9ors6ToVvc0Lom1oTz1KkxbLusHrycjDC5sQEWZRpPNOMu49qQT3RWMww18UXp5peXtG7cflX8BZMj\n",
    "TyQ=\n",
    "\"\"\",\n",
    "    \"station_forest.csv\": \"\"\"\n",
    "eNotkEtqg0EMg/c+i8b4MQ/PcQp/oVmkdJFScvuiTJYfsmXJ18cTj8/7D75+77fr9nji7/Z9iaNNXdiBriaBVodCTRJta6A2lk7p\n",
    "JEclXFMGyUimSyba0kFKnbLetJCaUvRMuriGbFJgO0yHuJ2DNRHq4q80RdW1xINGTqPXcB61JpLYj4rQJT7Qhk6Ohqb4fNfyVwRf\n",
    "xHE2Xby42VnTdYtvtGAIQ9cpYWjJarTaEv5GOx8KHjJsg2lJJFrnjxxdl0Sn6qhCUB3cHaiO0JCYaKaFVcd58e6kc2iXKA7nSRXy\n",
    "D7SAUsQ=\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",
    "Чтобы считать по ним вместе, части нужно **склеить** в один массив. Бывает и наоборот: месяц нужно\n",
    "**разрезать** на недели.\n",
    "\n",
    "## Продолжить массив: np.concatenate\n",
    "\n",
    "`np.concatenate([a, b, …])` ставит массивы друг за другом. Массивы передают **списком** — в квадратных скобках:"
   ],
   "id": "text-10"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Два массива в один\n",
    "import numpy as np\n",
    "\n",
    "a = np.array([1, 2, 3])\n",
    "b = np.array([10, 20])\n",
    "print(np.concatenate([a, b]))\n",
    "print(np.concatenate([b, a, b]))"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "concat"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 1. Январь и февраль\n",
    "\n",
    "В `jan` и `feb` — выручка кофейни за первые дни января и февраля. Сохраните в `all_sales` один массив: сначала\n",
    "январские дни, потом февральские, — а в `total` сумму выручки за все эти дни.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`np.concatenate([jan, feb])` — порядок в списке и есть порядок в результате.\n",
    "\n",
    "</details>"
   ],
   "id": "months-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "jan = np.array([410, 385, 402, 395, 430])\n",
    "feb = np.array([398, 441, 420, 415])\n",
    "all_sales = ...\n",
    "total = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "months"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 1 — выполните после своего решения\n",
    "def test_all():\n",
    "    \"all_sales — январь, потом февраль\"\n",
    "    assert isinstance(all_sales, np.ndarray), f\"all_sales — это {type(all_sales).__name__}, а нужен массив из np.concatenate\"\n",
    "    assert len(all_sales) == 9, f\"в all_sales {len(all_sales)} чисел, а должно быть 5 + 4 = 9\"\n",
    "    assert all_sales[0] == 410, \"all_sales начинается не с января: проверьте порядок частей\"\n",
    "    assert all_sales.tolist() == [410, 385, 402, 395, 430, 398, 441, 420, 415], f\"all_sales = {all_sales.tolist()}, а нужны все дни января, потом февраля\"\n",
    "\n",
    "\n",
    "def test_total():\n",
    "    \"total — выручка за все дни\"\n",
    "    assert total == 3696, f\"total = {total} — это не сумма всех девяти дней\"\n",
    "\n",
    "_check(test_all, test_total, names=[\"all_sales\",\"total\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "months-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "jan = np.array([410, 385, 402, 395, 430])\n",
    "feb = np.array([398, 441, 420, 415])\n",
    "all_sales = np.concatenate([jan, feb])\n",
    "total = all_sales.sum()\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "months-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "У двумерных массивов `concatenate` склеивает по оси `axis=` — как агрегаты: `axis=0` (по умолчанию) ставит\n",
    "таблицы друг под другом, добавляя строки, `axis=1` — рядом, добавляя столбцы. Размер по другой оси должен\n",
    "совпадать:"
   ],
   "id": "text-27"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Две таблицы: под и рядом\n",
    "t1 = np.array([[1, 2], [3, 4]])\n",
    "t2 = np.array([[5, 6], [7, 8]])\n",
    "print(np.concatenate([t1, t2]))            # axis=0: строки добавились\n",
    "print(np.concatenate([t1, t2], axis=1))    # axis=1: столбцы добавились"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "concat-2d"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Новая ось: np.stack\n",
    "\n",
    "Три метеостанции записали по 28 дней — три одномерных массива формы `(28,)`. Хочется таблицу 3 × 28: строка —\n",
    "станция. `concatenate` тут не поможет: он продолжит массив до 84 чисел. Нужна **новая ось** — её добавляет\n",
    "`np.stack`:"
   ],
   "id": "text-33"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# concatenate и stack\n",
    "center = np.genfromtxt(\"data/station_center.csv\", delimiter=\",\", skip_header=1)\n",
    "airport = np.genfromtxt(\"data/station_airport.csv\", delimiter=\",\", skip_header=1)\n",
    "forest = np.genfromtxt(\"data/station_forest.csv\", delimiter=\",\", skip_header=1)\n",
    "temps_long = np.concatenate([center[:, 1], airport[:, 1], forest[:, 1]])\n",
    "temps = np.stack([center[:, 1], airport[:, 1], forest[:, 1]])\n",
    "print(temps_long.shape)\n",
    "print(temps.shape)\n",
    "print(temps[:, :4])     # три станции, первые четыре дня"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "stack"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`np.stack` кладёт массивы одинаковой формы друг на друга: из нескольких `(n,)` получается `(k, n)`, где `k` —\n",
    "сколько массивов. С `axis=1` новой осью станут столбцы: `(n, k)`."
   ],
   "id": "text-41"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Вопрос.** `x` и `y` — массивы формы `(5,)`. Какая форма у `np.stack([x, y])`?\n",
    "\n",
    "- `(10,)`\n",
    "- `(2, 5)`\n",
    "- `(5, 2)`\n",
    "\n",
    "<details><summary>Ответ</summary>\n",
    "\n",
    "`(2, 5)`\n",
    "\n",
    "`stack` добавляет новую ось впереди: два массива по 5 чисел становятся двумя строками таблицы — `(2, 5)`. `(10,)` дал бы `concatenate`, а `(5, 2)` — `stack` с `axis=1`.\n",
    "\n",
    "</details>"
   ],
   "id": "quiz-45"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 2. Влажность трёх станций\n",
    "\n",
    "Загружены все три станции: `center`, `airport`, `forest`. Соберите в `hum` таблицу влажности (столбец 2 каждого\n",
    "файла): строка — станция в этом порядке, столбец — день. В `hum_avg` сохраните среднюю влажность каждой станции\n",
    "без учёта пропусков.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Столбец влажности — `center[:, 2]`; три таких столбца — в `np.stack([...])`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Средняя по станции — по строке: `np.nanmean(hum, axis=1)`.\n",
    "\n",
    "</details>"
   ],
   "id": "humidity-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "hum = ...\n",
    "hum_avg = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "humidity"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 2 — выполните после своего решения\n",
    "def test_hum():\n",
    "    \"hum — влажность: 3 станции × 28 дней\"\n",
    "    assert isinstance(hum, np.ndarray), f\"hum — это {type(hum).__name__}, а нужен массив из np.stack\"\n",
    "    assert hum.shape != (84,), \"hum — один длинный ряд из 84 чисел: concatenate продолжает массив, а таблицу строит np.stack\"\n",
    "    assert hum.shape != (28, 3), \"у hum форма (28, 3): станции должны быть строками\"\n",
    "    assert hum.shape == (3, 28), f\"у hum форма {hum.shape}, а нужна (3, 28)\"\n",
    "    assert np.nanmax(hum) <= 100 and np.nanmin(hum) >= 40, \"в hum не влажность: нужен столбец влажности каждой станции\"\n",
    "    assert np.isnan(hum[2]).sum() == 1, \"станции не в том порядке: center, airport, forest\"\n",
    "\n",
    "\n",
    "def test_avg():\n",
    "    \"hum_avg — средняя влажность каждой станции\"\n",
    "    assert not np.isnan(hum_avg).any(), \"в hum_avg есть nan: у леса пропуск — нужен np.nanmean\"\n",
    "    assert np.allclose(hum_avg, [74.821429, 81.5, 86.481481], atol=1e-5), f\"hum_avg = {hum_avg} — это не средняя влажность станций без учёта пропусков\"\n",
    "\n",
    "_check(test_hum, test_avg, names=[\"hum\",\"hum_avg\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "humidity-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "hum = np.stack([center[:, 2], airport[:, 2], forest[:, 2]])\n",
    "hum_avg = np.nanmean(hum, axis=1)\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "humidity-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Короче: np.vstack и np.hstack\n",
    "\n",
    "Для двух частых случаев есть короткие имена. `np.vstack` (vertical — вертикально) ставит массивы друг под\n",
    "другом, `np.hstack` (horizontal — горизонтально) — рядом. Для таблиц это `concatenate` с `axis=0` и `axis=1`,\n",
    "а одномерные массивы `vstack` делает строками — как `stack`:"
   ],
   "id": "text-63"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# vstack и hstack\n",
    "x = np.array([1, 2, 3])\n",
    "y = np.array([4, 5, 6])\n",
    "print(np.vstack([x, y]))\n",
    "print(np.hstack([x, y]))"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "vh"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Склеивать можно только подходящие куски: у `vstack` должна совпадать длина строк, у `hstack` — число строк."
   ],
   "id": "text-71"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Строки разной длины\n",
    "# Эта ячейка показывает ошибку ValueError: она перехвачена, чтобы «Выполнить всё» не останавливалось.\n",
    "try:\n",
    "    np.vstack([np.array([1, 2, 3]), np.array([4, 5])])\n",
    "except Exception as error:\n",
    "    print(f\"{type(error).__name__}: {error}\")"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "mismatch"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Частый случай — добавить к таблице столбец. Одномерный массив `hstack` к таблице не приклеит: у них разное число\n",
    "осей. Сначала превратите его в столбец `(n, 1)` — через `np.newaxis` из модуля о транслировании:"
   ],
   "id": "text-75"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Приклеить столбец\n",
    "scores = np.array([[70, 80], [65, 90], [88, 72]])\n",
    "bonus = np.array([5, 0, 3])\n",
    "print(np.hstack([scores, bonus[:, np.newaxis]]))"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "add-col"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Типичная ошибка: all the input arrays must have same number of dimensions.**\n",
    ">\n",
    "> Склеиваете таблицу `(n, k)` и одномерный массив `(n,)`. `hstack` ждёт две таблицы: сделайте из массива столбец —\n",
    "> `col[:, np.newaxis]`."
   ],
   "id": "component-81"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 3. Столбец с суммой\n",
    "\n",
    "В `order` — строки заказа: цена товара и количество. Сохраните в `table` ту же таблицу с третьим столбцом —\n",
    "суммой по строке (цена × количество).\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Суммы — `order[:, 0] * order[:, 1]`, это массив формы `(4,)`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "`np.hstack([order, суммы[:, np.newaxis]])`.\n",
    "\n",
    "</details>"
   ],
   "id": "add-column-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "order = np.array([[120, 3], [80, 5], [200, 1], [45, 12]])\n",
    "table = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "add-column"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 3 — выполните после своего решения\n",
    "def test_table():\n",
    "    \"table — заказ с третьим столбцом суммы\"\n",
    "    assert isinstance(table, np.ndarray), f\"table — это {type(table).__name__}, а нужен массив\"\n",
    "    assert table.shape != (5, 3) and table.shape != (3, 4), \"суммы стали строкой: нужен столбец\"\n",
    "    assert table.shape == (4, 3), f\"у table форма {table.shape}, а нужна (4, 3): четыре строки, три столбца\"\n",
    "    assert table[:, :2].tolist() == order.tolist(), \"первые два столбца table должны быть ценой и количеством из order\"\n",
    "    assert table[:, 2].tolist() == [360, 400, 200, 540], f\"третий столбец — {table[:, 2].tolist()}, а нужна сумма каждой строки: цена × количество\"\n",
    "\n",
    "_check(test_table, names=[\"table\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "add-column-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "order = np.array([[120, 3], [80, 5], [200, 1], [45, 12]])\n",
    "table = np.hstack([order, (order[:, 0] * order[:, 1])[:, np.newaxis]])\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "add-column-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Разрезать: np.split\n",
    "\n",
    "`np.split(a, n)` режет массив на `n` равных частей и возвращает **список** массивов. Части можно сразу\n",
    "разложить по переменным. Вместо числа частей можно передать список мест разреза — номера, с которых начинается\n",
    "следующая часть:"
   ],
   "id": "text-93"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Равные части и места разреза\n",
    "days = np.arange(1, 15)\n",
    "first, second = np.split(days, 2)\n",
    "print(first)\n",
    "print(second)\n",
    "print(np.split(days, [3, 10]))    # новые части начинаются с индексов 3 и 10"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "split"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Список массивов Python печатает как `[array([...]), array([...])]` — `array` здесь просто подпись: каждый\n",
    "элемент списка — массив NumPy.\n",
    "\n",
    "Если массив не делится на равные части, `split` не угадывает, куда деть остаток, а сообщает об ошибке:"
   ],
   "id": "text-101"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# 14 на 3 не делится\n",
    "# Эта ячейка показывает ошибку ValueError: она перехвачена, чтобы «Выполнить всё» не останавливалось.\n",
    "try:\n",
    "    np.split(days, 3)\n",
    "except Exception as error:\n",
    "    print(f\"{type(error).__name__}: {error}\")"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "uneven"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Список частей одной длины можно снова собрать `np.stack` — получится таблица, где строка — часть:"
   ],
   "id": "text-108"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 4. Февраль по неделям\n",
    "\n",
    "В `center_temp` — температура в центре за 28 дней февраля. Разрежьте её на четыре недели и соберите в `weeks`\n",
    "таблицу 4 × 7: строка — неделя. В `week_avg` сохраните среднюю температуру каждой недели без учёта пропусков.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`np.split(center_temp, 4)` — список из четырёх недель; `np.stack` делает из него таблицу.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Средняя по неделе — по строке: `np.nanmean(weeks, axis=1)`.\n",
    "\n",
    "</details>"
   ],
   "id": "weeks-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "center_temp = center[:, 1]\n",
    "weeks = ...\n",
    "week_avg = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "weeks"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 4 — выполните после своего решения\n",
    "def test_weeks():\n",
    "    \"weeks — таблица 4 × 7\"\n",
    "    assert not isinstance(weeks, list), \"weeks — список: np.split возвращает список частей, а нужна таблица\"\n",
    "    assert isinstance(weeks, np.ndarray), f\"weeks — это {type(weeks).__name__}, а нужен массив\"\n",
    "    assert weeks.shape != (7, 4), \"у weeks форма (7, 4): режьте на 4 части (недели), а не на 7\"\n",
    "    assert weeks.shape == (4, 7), f\"у weeks форма {weeks.shape}, а нужна (4, 7): строка — неделя\"\n",
    "    assert weeks[1, 0] == -5.5, \"в строках weeks не недели по порядку\"\n",
    "\n",
    "\n",
    "def test_avg():\n",
    "    \"week_avg — средняя каждой недели\"\n",
    "    assert np.shape(week_avg) == (4,), f\"у week_avg форма {np.shape(week_avg)}, а недель 4: нужна средняя по строкам weeks\"\n",
    "    assert not np.isnan(week_avg).any(), \"в week_avg есть nan: в первой неделе пропуск — нужен np.nanmean\"\n",
    "    assert np.allclose(week_avg, [-4.966667, -4.885714, -1.657143, -0.242857], atol=1e-5), f\"week_avg = {week_avg} — это не средние недель без учёта пропусков\"\n",
    "\n",
    "_check(test_weeks, test_avg, names=[\"weeks\",\"week_avg\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "weeks-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "center_temp = center[:, 1]\n",
    "weeks = np.stack(np.split(center_temp, 4))\n",
    "week_avg = np.nanmean(weeks, axis=1)\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "weeks-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ту же таблицу даёт `center_temp.reshape(4, 7)` из модуля об индексах. `split` удобнее, когда части разной длины\n",
    "(`np.split(a, [7, 14])`) или нужны отдельными переменными.\n",
    "\n",
    "## Итоги\n",
    "\n",
    "- `np.concatenate([a, b], axis=…)` продолжает массив по существующей оси.\n",
    "- `np.stack([a, b])` добавляет новую ось: из `k` массивов `(n,)` — таблица `(k, n)`.\n",
    "- `np.vstack` — друг под другом, `np.hstack` — рядом; размеры по другой оси должны совпадать.\n",
    "- Столбец к таблице — `np.hstack([table, col[:, np.newaxis]])`.\n",
    "- `np.split(a, n)` — `n` равных частей, `np.split(a, [i, j])` — разрез по местам; результат — список."
   ],
   "id": "text-118"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Подробнее в справочнике:** [numpy/join-split](https://ds-edu-trainer.vercel.app/reference/numpy/join-split)"
   ],
   "id": "more"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
