{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Таблица своими руками\n",
    "\n",
    "Собрать DataFrame из словаря списков или списка словарей, посмотреть типы столбцов, задать свои подписи строк и создать отдельный Series.\n",
    "\n",
    "Курс pandas · урок `pd-dataframe` · [урок на сайте](https://ds-edu-trainer.vercel.app/courses/pandas/dataframe).\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\",\"pandas\":\"3.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",
    "проверки идеи — проще собрать прямо в коде. Заодно станет понятнее, как DataFrame устроен внутри.\n",
    "\n",
    "## Из словаря списков\n",
    "\n",
    "Самый частый способ: словарь, где ключ — название столбца, а значение — список значений этого столбца."
   ],
   "id": "text-9"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Меню кофейни\n",
    "import pandas as pd\n",
    "\n",
    "menu = pd.DataFrame({\n",
    "    \"drink\": [\"эспрессо\", \"капучино\", \"латте\", \"какао\"],\n",
    "    \"price\": [150, 240, 260, 220],\n",
    "    \"volume\": [0.06, 0.25, 0.35, 0.3],\n",
    "})\n",
    "menu"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "dict"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Получилась обычная таблица: у неё есть `shape`, столбцы достаются скобками и умеют всё то же, что столбцы\n",
    "таблицы из файла."
   ],
   "id": "text-18"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Размер и средняя цена\n",
    "print(menu.shape)\n",
    "print(menu[\"price\"].mean())"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "use"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Списки должны быть одной длины — в таблице не бывает столбцов разной высоты:"
   ],
   "id": "text-23"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Три напитка, две цены\n",
    "# Эта ячейка показывает ошибку ValueError: она перехвачена, чтобы «Выполнить всё» не останавливалось.\n",
    "try:\n",
    "    pd.DataFrame({\n",
    "        \"drink\": [\"эспрессо\", \"капучино\", \"латте\"],\n",
    "        \"price\": [150, 240],\n",
    "    })\n",
    "except Exception as error:\n",
    "    print(f\"{type(error).__name__}: {error}\")"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "uneven"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Типичная ошибка: All arrays must be of the same length.**\n",
    ">\n",
    "> Ошибка значит, что списки в словаре разной длины. Посчитайте элементы в каждом: скорее всего, где-то\n",
    "> пропущено значение или запятая."
   ],
   "id": "component-28"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 1. Сотрудники\n",
    "\n",
    "Создайте таблицу `staff` с тремя столбцами: `name` — Анна, Олег, Мария; `role` — бариста, бариста, управляющий;\n",
    "`salary` — 62000, 58000, 95000.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`pd.DataFrame({\"name\": [...], \"role\": [...], \"salary\": [...]})` — три ключа, три списка по три значения.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Текст пишут в кавычках, числа — без кавычек.\n",
    "\n",
    "</details>"
   ],
   "id": "staff-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "staff = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "staff"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 1 — выполните после своего решения\n",
    "def test_frame():\n",
    "    \"staff — таблица 3 × 3\"\n",
    "    assert not isinstance(staff, dict), \"staff — словарь, а нужна таблица\"\n",
    "    assert isinstance(staff, pd.DataFrame), f\"staff — это {type(staff).__name__}, а нужна таблица\"\n",
    "    assert list(staff.columns) == [\"name\", \"role\", \"salary\"], f\"столбцы сейчас {list(staff.columns)}, а нужны name, role, salary — в этом порядке\"\n",
    "    assert len(staff) == 3, f\"в staff {len(staff)} строк, а сотрудников трое\"\n",
    "\n",
    "\n",
    "def test_values():\n",
    "    \"в staff — данные трёх сотрудников\"\n",
    "    assert isinstance(staff, pd.DataFrame) and list(staff.columns) == [\"name\", \"role\", \"salary\"], \"сначала исправьте то, о чём говорит проверка выше\"\n",
    "    assert staff[\"name\"].tolist() == [\"Анна\", \"Олег\", \"Мария\"], f\"в столбце name сейчас {staff['name'].tolist()}, а нужно Анна, Олег, Мария\"\n",
    "    assert staff[\"role\"].tolist() == [\"бариста\", \"бариста\", \"управляющий\"], f\"в столбце role сейчас {staff['role'].tolist()}\"\n",
    "    assert staff[\"salary\"].tolist() != [\"62000\", \"58000\", \"95000\"], \"зарплаты записаны строками в кавычках — с ними нельзя считать; пишите числа без кавычек\"\n",
    "    assert staff[\"salary\"].tolist() == [62000, 58000, 95000], f\"в столбце salary сейчас {staff['salary'].tolist()}\"\n",
    "\n",
    "_check(test_frame, test_values, names=[\"staff\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "staff-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "staff = pd.DataFrame({\n",
    "    \"name\": [\"Анна\", \"Олег\", \"Мария\"],\n",
    "    \"role\": [\"бариста\", \"бариста\", \"управляющий\"],\n",
    "    \"salary\": [62000, 58000, 95000],\n",
    "})\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "staff-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Типы столбцов\n",
    "\n",
    "В одном столбце лежат значения одного типа. Типы всех столбцов показывает атрибут `dtypes`:"
   ],
   "id": "text-40"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Типы столбцов меню\n",
    "menu.dtypes"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "dtypes"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "pandas определил типы сам по значениям: текст — `str`, целые числа — `int64`, дробные — `float64`. Число 64 —\n",
    "сколько бит памяти занимает одно значение; помнить его не нужно. Слово `object` в последней строке относится\n",
    "к самому списку типов, а не к таблице.\n",
    "\n",
    "Тип важен: с числовым столбцом можно считать, с текстовым — нет. Если числа попали в таблицу как текст\n",
    "(в кавычках), `sum` склеит их как строки, а `mean` упадёт с ошибкой. Как чинить такие столбцы, разберём\n",
    "в модуле об очистке данных."
   ],
   "id": "text-46"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Вопрос.** В списке `[1, 2.5, 3]` есть и целые, и дробное число. Какой тип получит столбец `x` в\n",
    "`pd.DataFrame({\"x\": [1, 2.5, 3]})`?\n",
    "\n",
    "- `int64`\n",
    "- `float64`\n",
    "- `str`\n",
    "\n",
    "<details><summary>Ответ</summary>\n",
    "\n",
    "`float64`\n",
    "\n",
    "Тип у столбца один. Целое можно без потерь записать дробным (1 → 1.0), а дробное целым — нельзя, поэтому pandas выбирает `float64`.\n",
    "\n",
    "</details>"
   ],
   "id": "quiz-55"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Из списка словарей\n",
    "\n",
    "Второй способ — список, в котором каждый словарь описывает одну **строку**. Так часто выглядят данные,\n",
    "пришедшие из интернета или собранные в цикле:"
   ],
   "id": "text-65"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Строка за строкой\n",
    "visits = pd.DataFrame([\n",
    "    {\"day\": \"пн\", \"guests\": 84, \"rain\": False},\n",
    "    {\"day\": \"вт\", \"guests\": 91, \"rain\": True},\n",
    "    {\"day\": \"ср\", \"guests\": 77, \"rain\": True},\n",
    "])\n",
    "visits"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "records"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Названия столбцов pandas взял из ключей. Появился ещё один тип: `bool` — логические значения `True` и `False`."
   ],
   "id": "text-72"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 2. Остатки на складе\n",
    "\n",
    "Создайте таблицу `stock` из списка словарей — три строки со столбцами `item` и `liters`: молоко — 12.5,\n",
    "сливки — 3.0, сироп — 1.5. Затем сохраните в `total_liters`, сколько всего литров на складе.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`pd.DataFrame([{\"item\": \"молоко\", \"liters\": 12.5}, {...}, {...}])`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Всего литров — сумма столбца: `stock[\"liters\"].sum()`.\n",
    "\n",
    "</details>"
   ],
   "id": "stock-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "stock = ...\n",
    "total_liters = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "stock"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 2 — выполните после своего решения\n",
    "def test_stock():\n",
    "    \"stock — таблица из трёх строк\"\n",
    "    assert isinstance(stock, pd.DataFrame), f\"stock — это {type(stock).__name__}, а нужна таблица\"\n",
    "    assert list(stock.columns) == [\"item\", \"liters\"], f\"столбцы сейчас {list(stock.columns)}, а нужны item и liters\"\n",
    "    assert stock[\"item\"].tolist() == [\"молоко\", \"сливки\", \"сироп\"], f\"в столбце item сейчас {stock['item'].tolist()}\"\n",
    "    assert stock[\"liters\"].tolist() == [12.5, 3.0, 1.5], f\"в столбце liters сейчас {stock['liters'].tolist()}\"\n",
    "\n",
    "\n",
    "def test_total():\n",
    "    \"total_liters — сколько всего литров\"\n",
    "    assert not isinstance(total_liters, pd.Series), \"total_liters — столбец, а нужно одно число: сумма столбца liters\"\n",
    "    assert abs(total_liters - 17.0) < 1e-9, f\"total_liters = {total_liters} — это не сумма столбца liters\"\n",
    "\n",
    "_check(test_stock, test_total, names=[\"stock\",\"total_liters\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "stock-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "stock = pd.DataFrame([\n",
    "    {\"item\": \"молоко\", \"liters\": 12.5},\n",
    "    {\"item\": \"сливки\", \"liters\": 3.0},\n",
    "    {\"item\": \"сироп\", \"liters\": 1.5},\n",
    "])\n",
    "total_liters = stock[\"liters\"].sum()\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "stock-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Свои подписи строк\n",
    "\n",
    "До сих пор индекс был номерами 0, 1, 2… Параметр `index=` задаёт свои подписи строк — **метки** (labels):"
   ],
   "id": "text-82"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Размеры стаканов\n",
    "sizes = pd.DataFrame(\n",
    "    {\"volume\": [0.25, 0.35, 0.45], \"extra\": [0, 30, 60]},\n",
    "    index=[\"S\", \"M\", \"L\"],\n",
    ")\n",
    "sizes"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "labels"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Индекс не входит в число столбцов: у `sizes` два столбца, а `S`, `M`, `L` — подписи строк. Сам индекс хранится\n",
    "в атрибуте `index`:"
   ],
   "id": "text-88"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Индекс с метками и индекс из номеров\n",
    "print(sizes.index)\n",
    "print(menu.index)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "labels-index"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Series без таблицы\n",
    "\n",
    "Один столбец тоже можно создать отдельно — `pd.Series` из списка. Параметр `index=` задаёт метки, `name=` —\n",
    "название:"
   ],
   "id": "text-93"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Чаевые по дням\n",
    "tips = pd.Series([120, 80, 200, 150], index=[\"пн\", \"вт\", \"ср\", \"чт\"], name=\"tips\")\n",
    "tips"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "series"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "По метке значение достают квадратными скобками — как из словаря:"
   ],
   "id": "text-100"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Значение по метке и сумма\n",
    "print(tips[\"ср\"])\n",
    "print(tips.sum())"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "by-label"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Series — это пары «метка → значение» с общим типом и названием. А DataFrame — несколько Series с **общим\n",
    "индексом**, поставленные рядом: каждый столбец — Series. Поэтому `menu[\"price\"]` возвращает именно Series."
   ],
   "id": "text-104"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 3. Гости за неделю\n",
    "\n",
    "Создайте Series `guests` с названием `guests`: значения 84, 91, 77, 102, 130, метки — пн, вт, ср, чт, пт.\n",
    "Сохраните в `friday` число гостей в пятницу, достав его из `guests` по метке.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`pd.Series([...], index=[...], name=\"guests\")`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "По метке: `guests[\"пт\"]`.\n",
    "\n",
    "</details>"
   ],
   "id": "week-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "guests = ...\n",
    "friday = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "week"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 3 — выполните после своего решения\n",
    "def test_series():\n",
    "    \"guests — Series с днями недели в индексе\"\n",
    "    assert isinstance(guests, pd.Series), f\"guests — это {type(guests).__name__}, а нужен Series\"\n",
    "    assert guests.tolist() == [84, 91, 77, 102, 130], f\"значения сейчас {guests.tolist()}, а нужны 84, 91, 77, 102, 130\"\n",
    "    assert list(guests.index) != [0, 1, 2, 3, 4], \"индекс — номера 0…4, а нужны дни недели: вспомните параметр index\"\n",
    "    assert list(guests.index) == [\"пн\", \"вт\", \"ср\", \"чт\", \"пт\"], f\"индекс сейчас {list(guests.index)}, а нужен пн, вт, ср, чт, пт\"\n",
    "    assert guests.name == \"guests\", f\"название сейчас {guests.name!r}, а нужно \\\"guests\\\"\"\n",
    "\n",
    "\n",
    "def test_friday():\n",
    "    \"friday — гостей в пятницу\"\n",
    "    assert friday == 130, f\"friday = {friday!r}: значение берут по метке индекса\"\n",
    "\n",
    "_check(test_series, test_friday, names=[\"guests\",\"friday\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "week-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "guests = pd.Series([84, 91, 77, 102, 130], index=[\"пн\", \"вт\", \"ср\", \"чт\", \"пт\"], name=\"guests\")\n",
    "friday = guests[\"пт\"]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "week-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Итоги\n",
    "\n",
    "- `pd.DataFrame({\"столбец\": [значения], ...})` — таблица из словаря списков; списки одной длины.\n",
    "- `pd.DataFrame([{...}, {...}])` — таблица из списка словарей: словарь — строка.\n",
    "- `dtypes` — типы столбцов: `int64`, `float64`, `str`, `bool`.\n",
    "- `index=` задаёт метки строк; `pd.Series(значения, index=..., name=...)` — отдельный столбец.\n",
    "- DataFrame — это несколько Series с общим индексом."
   ],
   "id": "text-115"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Подробнее в справочнике:** [pandas/dataframe](https://ds-edu-trainer.vercel.app/reference/pandas/dataframe) · [pandas/series](https://ds-edu-trainer.vercel.app/reference/pandas/series) · [pandas/dtypes](https://ds-edu-trainer.vercel.app/reference/pandas/dtypes)"
   ],
   "id": "more"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
