{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Изменить значения\n",
    "\n",
    "Записать значение в ячейку и в строки по условию через loc; почему запись в два шага df[маска][столбец] = … ничего не меняет; правило Copy-on-Write и явная копия copy().\n",
    "\n",
    "Курс pandas · урок `pd-set-values` · [урок на сайте](https://ds-edu-trainer.vercel.app/courses/pandas/set-values).\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",
    "_DATA = {\n",
    "    \"products.csv\": \"\"\"\n",
    "eNp9k01qG0EQhfc6RR+gDNU9P5q5hW9gjCyCF4mM4iyym7EMCligeBUwgRDH4GwCI1tjj35mfIVXNwrV4/hPIsvu+V6/qlc1R8PB\n",
    "wafe8d7hAX3Yf9+n3v5x/91g+JmOhoe9PvUGH487u2wJfyTHg2QoJZcczY7kWPvDxFiDJW4JF2jkFCXZMGJKutzZZedvsZIR1pih\n",
    "kqlxEZuXdJwyBYnCgbqcokKDh61oN2UKA0VDwrlkKHCPCisPb1ThUqaufzh6ujWYocS9ov6MCjWKTSPHFFqVxoRrUatazrAwMkaB\n",
    "hbHc8td6oiBispHSXcI3lFjJ+X/5lMn6eBLCLx9O/QaJQiYXK5ISLn2nNzJFjUYfzTFDI5mJXkpczGSZO7uWCb/RaItYocDc4FZx\n",
    "mWCJCgvCz/YejeRygopswpSq0hJ+oJQxStQy0bga3Ej+6FxuUUZM2ol1hO8+zDkKrGRicGPky8sqtskD95iEDXxyOn3JNjln//UW\n",
    "Ei4kkxHusERhAo1gjZXWrfBI/SlKmZwOxEaEK11a1DLeed7f17hNUqbU87GmPZLMP64LjrkuiExfK1zITDbwFXUfB4AaFZYG9zor\n",
    "FJKhkXG7Bm/sdF/iULXJ08ph7XNSW21Pla3tVywlb6uWkT4rZxobk029fUq40n9AJnKiHw1m/mcrcOfNy+1PuLAduWPCJUrJfAHL\n",
    "52i3y6wOPIq58xfQUEqA\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",
    "`loc` слева от знака `=`.\n",
    "\n",
    "## Одна ячейка\n",
    "\n",
    "`loc[метка строки, название столбца]` не только читает ячейку, но и записывает в неё:"
   ],
   "id": "text-10"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Новая цена шоколада\n",
    "import pandas as pd\n",
    "\n",
    "products = pd.read_csv(\"data/products.csv\")\n",
    "products.loc[9, \"price\"] = 190\n",
    "products.loc[9]"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "cell"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Строки по условию\n",
    "\n",
    "На месте метки строки может стоять маска. Тогда значение запишется во все строки, где в маске `True`:"
   ],
   "id": "text-20"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Весь чай — по 400 ₽\n",
    "is_tea = products[\"category\"] == \"Чай\"\n",
    "products.loc[is_tea, \"price\"] = 400\n",
    "products[is_tea]"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "by-mask"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Справа может стоять не одно значение, а Series — например, прежние цены тех же строк плюс 20 ₽:"
   ],
   "id": "text-26"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Сладости дорожают на 20 ₽\n",
    "is_sweet = products[\"category\"] == \"Сладости\"\n",
    "products.loc[is_sweet, \"price\"] = products.loc[is_sweet, \"price\"] + 20\n",
    "products[is_sweet]"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "by-mask-calc"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Читается так: «в строках-сладостях в столбец `price` записать прежние цены этих строк, увеличенные на 20».\n",
    "То же самое короче: `products.loc[is_sweet, \"price\"] += 20`."
   ],
   "id": "text-30"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 1. Исправить опечатку\n",
    "\n",
    "В справочнике ошибка: у товара в строке с меткой 12 (зефир) цена должна быть 250, а себестоимость — 120.\n",
    "Исправьте оба значения в таблице `fixed`.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`fixed.loc[12, \"price\"] = 250` — и так же для столбца `cost`.\n",
    "\n",
    "</details>"
   ],
   "id": "fix-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "fixed = pd.read_csv(\"data/products.csv\")\n",
    "# исправьте цену и себестоимость в строке с меткой 12"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "fix"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 1 — выполните после своего решения\n",
    "def test_fixed():\n",
    "    \"в строке 12 цена 250 и себестоимость 120\"\n",
    "    assert isinstance(fixed, pd.DataFrame) and fixed.shape == (20, 5), \"fixed должна остаться таблицей товаров: 20 строк, 5 столбцов\"\n",
    "    assert fixed.loc[12, \"price\"] != 210 or fixed.loc[12, \"cost\"] != 100, \"строка 12 не изменилась\"\n",
    "    assert fixed.loc[12, \"price\"] == 250, f\"цена в строке 12 — {fixed.loc[12, 'price']}, а нужна 250\"\n",
    "    assert fixed.loc[12, \"cost\"] == 120, f\"себестоимость в строке 12 — {fixed.loc[12, 'cost']}, а нужна 120\"\n",
    "\n",
    "\n",
    "def test_rest():\n",
    "    \"остальные строки не изменились\"\n",
    "    assert fixed[\"price\"].sum() == 18160 + 40, \"изменились цены в других строках: менять нужно только строку с меткой 12\"\n",
    "    assert fixed[\"cost\"].sum() == 9670 + 20, \"изменилась себестоимость в других строках: менять нужно только строку с меткой 12\"\n",
    "\n",
    "_check(test_fixed, test_rest)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "fix-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "fixed = pd.read_csv(\"data/products.csv\")\n",
    "fixed.loc[12, \"price\"] = 250\n",
    "fixed.loc[12, \"cost\"] = 120\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "fix-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Так же меняют текст. Отметим дорогие товары отдельной категорией:"
   ],
   "id": "text-40"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Товары дороже 2000 ₽ — в «Премиум»\n",
    "products.loc[products[\"price\"] > 2000, \"category\"] = \"Премиум\"\n",
    "products[\"category\"].value_counts()"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "text"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Запись в два шага не работает\n",
    "\n",
    "Мы умеем фильтровать: `таблица[маска]`. И умеем брать столбец: `таблица[\"price\"]`. Напрашивается соединить —\n",
    "отфильтровать, взять столбец и присвоить. Попробуем обнулить цены чая в свежей таблице:"
   ],
   "id": "text-44"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Два шага подряд\n",
    "fresh = pd.read_csv(\"data/products.csv\")\n",
    "fresh[fresh[\"category\"] == \"Чай\"][\"price\"] = 0"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "chained"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Несмотря на слово `Error` в названии, это **предупреждение**, а не ошибка: программа не остановилась,\n",
    "следующие строки выполнились бы. pandas сообщает, что значение записано **в копию**. Проверим — цены чая прежние:"
   ],
   "id": "text-51"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Ничего не изменилось\n",
    "fresh[fresh[\"category\"] == \"Чай\"]"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "chained-result"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Почему так? Запись состоит из двух шагов. Первый — `fresh[маска]` — создаёт **новую таблицу** из отобранных\n",
    "строк. Второй — `[\"price\"] = 0` — записывает нули в эту новую таблицу. Но она не сохранена ни в какой\n",
    "переменной и сразу исчезает. До `fresh` запись не доходит."
   ],
   "id": "text-56"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Типичная ошибка: ChainedAssignmentError.**\n",
    ">\n",
    "> Две пары скобок подряд слева от `=` — `таблица[...][...] = значение` — это запись в два шага, она никогда\n",
    "> не меняет исходную таблицу. Делайте один шаг: `таблица.loc[маска, \"столбец\"] = значение`.\n",
    ">\n",
    "> Два уточнения. **Читать** двумя парами скобок можно: `таблица[маска][\"price\"].mean()` — обычная запись,\n",
    "> проблема только в присваивании. А `таблица.loc[маска][\"price\"] = значение` — тоже два шага, хотя там есть\n",
    "> `loc`: строки и столбец должны стоять **в одних** скобках."
   ],
   "id": "component-61"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 2. Чай дорожает\n",
    "\n",
    "В таблице `sale` поднимите цену всех товаров категории «Чай» на 30 ₽. Остальные цены не меняйте.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Маска — `sale[\"category\"] == \"Чай\"`. Слева от `=` — `sale.loc[маска, \"price\"]`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Справа — те же цены плюс 30: `sale.loc[маска, \"price\"] + 30`. Или короче — оператор `+=`.\n",
    "\n",
    "</details>"
   ],
   "id": "tea-up-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "sale = pd.read_csv(\"data/products.csv\")\n",
    "# поднимите цену всего чая на 30 ₽"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "tea-up"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 2 — выполните после своего решения\n",
    "def test_tea():\n",
    "    \"чай подорожал на 30 ₽\"\n",
    "    assert isinstance(sale, pd.DataFrame) and sale.shape == (20, 5), \"sale должна остаться таблицей товаров: 20 строк, 5 столбцов\"\n",
    "    tea = sale.loc[sale[\"category\"] == \"Чай\", \"price\"].tolist()\n",
    "    assert tea != [350, 390, 540, 260], \"цены чая не изменились. Если вы писали sale[...][\\\"price\\\"] = ..., это запись в два шага: она меняет временную копию. Нужна запись в один шаг — через loc\"\n",
    "    assert tea != [30, 30, 30, 30], \"цены чая стали равны 30, а нужно прибавить 30 к прежним ценам\"\n",
    "    assert tea == [380, 420, 570, 290], f\"цены чая сейчас {tea}, а нужно прибавить 30 к прежним ценам чая\"\n",
    "\n",
    "\n",
    "def test_rest():\n",
    "    \"остальные цены не изменились\"\n",
    "    rest = sale.loc[sale[\"category\"] != \"Чай\", \"price\"].sum()\n",
    "    assert rest == 18160 - 1540, \"изменились цены не только чая: слева должна стоять маска категории «Чай»\"\n",
    "\n",
    "_check(test_tea, test_rest)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "tea-up-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "sale = pd.read_csv(\"data/products.csv\")\n",
    "sale.loc[sale[\"category\"] == \"Чай\", \"price\"] = sale.loc[sale[\"category\"] == \"Чай\", \"price\"] + 30\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "tea-up-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Copy-on-Write: всё, что получено из таблицы, — отдельная таблица\n",
    "\n",
    "За этим стоит простое правило pandas 3. Оно называется **Copy-on-Write** («копирование при записи»: настоящая\n",
    "копия данных создаётся не сразу, а в тот момент, когда в объект что-то записывают):\n",
    "\n",
    "> Любая таблица или Series, полученные из другой таблицы, ведут себя как независимая копия. Изменение\n",
    "> одного объекта никогда не меняет другой.\n",
    "\n",
    "Отобрали строки в отдельную переменную и изменили их — исходная таблица не заметит:"
   ],
   "id": "text-76"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Чай отдельно, таблица отдельно\n",
    "tea = fresh[fresh[\"category\"] == \"Чай\"]\n",
    "tea[\"price\"] = 0\n",
    "print(tea[\"price\"].tolist())\n",
    "print(fresh.loc[fresh[\"category\"] == \"Чай\", \"price\"].tolist())"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "subset"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "То же со столбцом: `fresh[\"price\"]` — отдельный Series, запись в него таблицу не меняет."
   ],
   "id": "text-88"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Столбец тоже сам по себе\n",
    "prices = fresh[\"price\"]\n",
    "prices.loc[0] = 1\n",
    "print(prices.loc[0])\n",
    "print(fresh.loc[0, \"price\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "column-copy"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Правило удобно тем, что у него нет исключений: чтобы изменить таблицу, нужно обратиться **к ней самой** —\n",
    "`таблица.loc[...] = ...` или `таблица[\"столбец\"] = ...`. Случайно испортить её через выборку нельзя."
   ],
   "id": "text-92"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **В старых версиях pandas было иначе.** До версии 3 такая запись иногда меняла исходную таблицу, а иногда нет — в зависимости от деталей. pandas\n",
    "> показывал предупреждение `SettingWithCopyWarning`, которое пугало новичков годами. В старых учебниках и ответах\n",
    "> в интернете оно встречается часто. В pandas 3 неопределённости больше нет."
   ],
   "id": "component-96"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Вопрос.** ```python\n",
    "t = pd.DataFrame({\"x\": [1, 2, 3]})\n",
    "part = t[t[\"x\"] > 1]\n",
    "part[\"x\"] = 100\n",
    "print(t[\"x\"].sum())\n",
    "```\n",
    "\n",
    "Что выведет этот код?\n",
    "\n",
    "- `6`\n",
    "- `201`\n",
    "- `300`\n",
    "\n",
    "<details><summary>Ответ</summary>\n",
    "\n",
    "`6`\n",
    "\n",
    "`part` получена из `t` фильтром — это независимая таблица. Запись в `part` не меняет `t`: в ней по-прежнему 1, 2, 3, сумма 6.\n",
    "\n",
    "</details>"
   ],
   "id": "quiz-102"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Второе имя — не копия\n",
    "\n",
    "Один случай выглядит как исключение, но он не про pandas, а про сам Python. Присваивание `same = fresh` ничего\n",
    "не получает «из таблицы»: оно даёт **той же** таблице второе имя."
   ],
   "id": "text-118"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Два имени одной таблицы\n",
    "same = fresh\n",
    "same.loc[0, \"price\"] = 1\n",
    "print(fresh.loc[0, \"price\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "alias"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Так же ведут себя списки и словари Python. Когда нужна настоящая независимая копия — например, чтобы\n",
    "поэкспериментировать и не испортить исходные данные, — её создаёт метод `copy`:"
   ],
   "id": "text-125"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Копия живёт отдельно\n",
    "backup = fresh.copy()\n",
    "backup.loc[0, \"price\"] = 5000\n",
    "print(backup.loc[0, \"price\"])\n",
    "print(fresh.loc[0, \"price\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "copy"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 3. Черновик\n",
    "\n",
    "Сохраните в `draft` независимую копию таблицы `original` и обнулите в `draft` весь столбец `price`.\n",
    "Таблица `original` должна остаться прежней.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`draft = original.copy()`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Весь столбец заменяет присваивание `draft[\"price\"] = 0`.\n",
    "\n",
    "</details>"
   ],
   "id": "draft-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "original = pd.read_csv(\"data/products.csv\")\n",
    "draft = ...\n",
    "# обнулите столбец price в draft"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "draft"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 3 — выполните после своего решения\n",
    "def test_draft():\n",
    "    \"draft — копия с нулевыми ценами\"\n",
    "    assert isinstance(draft, pd.DataFrame), f\"draft — это {type(draft).__name__}, а нужна таблица: копия original\"\n",
    "    assert draft.shape == (20, 5), f\"у draft размер {draft.shape}, а должен быть (20, 5) — как у original\"\n",
    "    assert draft[\"price\"].sum() == 0, \"цены в draft не обнулены\"\n",
    "    assert draft[\"cost\"].sum() == 9670, \"в draft изменилась себестоимость, а обнулить нужно только price\"\n",
    "\n",
    "\n",
    "def test_original():\n",
    "    \"original не изменилась\"\n",
    "    assert draft is not original, \"draft и original — одна и та же таблица под двумя именами: draft = original не создаёт копию. Нужна копия\"\n",
    "    assert original[\"price\"].sum() == 18160, \"цены в original изменились, а она должна остаться исходной\"\n",
    "\n",
    "_check(test_draft, test_original, names=[\"draft\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "draft-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "original = pd.read_csv(\"data/products.csv\")\n",
    "draft = original.copy()\n",
    "draft[\"price\"] = 0\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "draft-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Дробное число в целый столбец\n",
    "\n",
    "У столбца один тип. В столбец целых чисел (`int64`) pandas 3 откажется записать дробное:"
   ],
   "id": "text-138"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# 699.9 в столбец целых\n",
    "# Эта ячейка показывает ошибку TypeError: она перехвачена, чтобы «Выполнить всё» не останавливалось.\n",
    "try:\n",
    "    fresh.loc[1, \"price\"] = 699.9\n",
    "except Exception as error:\n",
    "    print(f\"{type(error).__name__}: {error}\")"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "float-error"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Старые версии pandas в такой ситуации молча меняли тип всего столбца на дробный — и целые цены вдруг становились\n",
    "`1450.0`. Теперь тип нужно сменить явно: сначала столбец переводят в дробный. Это делает метод `astype` — подробно о типах поговорим в модуле об очистке:"
   ],
   "id": "text-144"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Сначала тип, потом значение\n",
    "fresh[\"price\"] = fresh[\"price\"].astype(float)\n",
    "fresh.loc[1, \"price\"] = 699.9\n",
    "fresh.head(3)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "astype"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "(Цена `1.0` в первой строке — след примера с двумя именами выше.)"
   ],
   "id": "text-149"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Типичная ошибка: Invalid value … for dtype 'int64'.**\n",
    ">\n",
    "> Ошибка значит: в столбец целых чисел записывают дробное значение. Чаще всего она появляется при умножении\n",
    "> цен на процент. Переведите столбец в дробный тип — `таблица[\"столбец\"] = таблица[\"столбец\"].astype(float)` —\n",
    "> и повторите запись."
   ],
   "id": "component-152"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 4. Кофе дорожает на 7.5 %\n",
    "\n",
    "В таблице `raised` поднимите цену всех товаров категории «Кофе» на 7.5 %: новая цена — прежняя, умноженная\n",
    "на 1.075. Остальные цены не меняйте.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Цены станут дробными — сначала `raised[\"price\"] = raised[\"price\"].astype(float)`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Затем `raised.loc[маска, \"price\"] = raised.loc[маска, \"price\"] * 1.075`.\n",
    "\n",
    "</details>"
   ],
   "id": "coffee-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "raised = pd.read_csv(\"data/products.csv\")\n",
    "# поднимите цену кофе на 7.5 %"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "coffee"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 4 — выполните после своего решения\n",
    "def test_coffee():\n",
    "    \"кофе подорожал на 7.5 %\"\n",
    "    assert isinstance(raised, pd.DataFrame) and raised.shape == (20, 5), \"raised должна остаться таблицей товаров: 20 строк, 5 столбцов\"\n",
    "    coffee = raised.loc[raised[\"category\"] == \"Кофе\", \"price\"].tolist()\n",
    "    assert coffee != [1450, 690, 790, 1290, 720], \"цены кофе не изменились: записывайте через loc с маской\"\n",
    "    expected = [1558.75, 741.75, 849.25, 1386.75, 774.0]\n",
    "    assert all(abs(a - b) < 1e-6 for a, b in zip(coffee, expected)), f\"цены кофе сейчас {coffee}: прежние цены кофе нужно поднять на 7.5 %\"\n",
    "\n",
    "\n",
    "def test_rest():\n",
    "    \"остальные цены не изменились\"\n",
    "    rest = raised.loc[raised[\"category\"] != \"Кофе\", \"price\"].sum()\n",
    "    assert abs(rest - (18160 - 4940)) < 1e-6, \"изменились цены не только кофе: слева должна стоять маска категории «Кофе»\"\n",
    "\n",
    "_check(test_coffee, test_rest)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "coffee-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "raised = pd.read_csv(\"data/products.csv\")\n",
    "raised[\"price\"] = raised[\"price\"].astype(float)\n",
    "is_coffee = raised[\"category\"] == \"Кофе\"\n",
    "raised.loc[is_coffee, \"price\"] = raised.loc[is_coffee, \"price\"] * 1.075\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "coffee-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Итоги\n",
    "\n",
    "- Изменить часть значений: `таблица.loc[строки, \"столбец\"] = значение` — строки задаёт метка или маска.\n",
    "- `таблица[маска][\"столбец\"] = …` — запись в два шага: меняется временная копия, исходная таблица — нет.\n",
    "- Copy-on-Write: всё, что получено из таблицы, ведёт себя как независимая копия.\n",
    "- `b = a` — второе имя той же таблицы; независимая копия — `a.copy()`.\n",
    "- В целый столбец нельзя записать дробное число — сначала `astype(float)`."
   ],
   "id": "text-165"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Подробнее в справочнике:** [pandas/copy-on-write](https://ds-edu-trainer.vercel.app/reference/pandas/copy-on-write) · [pandas/loc-iloc](https://ds-edu-trainer.vercel.app/reference/pandas/loc-iloc) · [pandas/mistakes](https://ds-edu-trainer.vercel.app/reference/pandas/mistakes)"
   ],
   "id": "more"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
