{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Мини-проект: климат двух городов\n",
    "\n",
    "Сравнить Сочи и Новосибирск по данным за год: средняя температура и её разброс, доли морозных и жарких дней, осадки, годовая амплитуда — и найти зимние оттепели в Москве.\n",
    "\n",
    "Курс pandas · урок `pd-project-climate` · [урок на сайте](https://ds-edu-trainer.vercel.app/courses/pandas/project-climate).\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",
    "    \"weather.csv\": \"\"\"\n",
    "eNqVfUuuBr1x3dxr+e4Fi2+uRjBsDTSQISQCkswke5BJVhDYCLwD2YERQUC0hvvvKGhWdbNOVb8yuMAd8OtuksVTxWKdw7//29//\n",
    "+vN3v/n9f/v8/te//d2vfvubf5B//va/fn73n379d7/53a9++9vPf/nNP/z9r377n/8mhli+An0F+vz8889ff/njz19+/v3nT5+v\n",
    "8T0+X+k7fsJ3+ND3WC2ja9lWy/idVsvkWoatZZsty3dcLbNpSfk7fr7ad/zQd97+VtNim8bvyk23pybdtPqmeWvaP5+q3958u/75\n",
    "6t9BHllX026a9u/y+crHy9tqOUzLtnV+e3D4Tp/0TUdLCqZl3YaejmeuoafTSSqzJX3qd1kt/SSl7e1NnrkmidLZM/M3fWhO5+o7\n",
    "uUkK2yTl7+RGnop7KG2vz2IjfbWsbpjy1nJ75vaxa0CpuaHP2zD1T/8eOEzdvb3w0MdtVKH34+yh6WTsYzh7aJofus3pGqdIrkt9\n",
    "e/32vG0prZmP8ezt8XvIolvDFJNrORddkZZr6GN21lR5mOzQRztJWUadvtMnfofV0M/RfDnPZtMvb95EZtPuTCT2+x6p8Rz+oYVN\n",
    "NBgTTX6S+rZAtj71T1FDn+gURro8NB3wED00Utia7u9vx5DGM2yktezaYc3RY2PlqQ+CjXW1zHct07Hoo4fGzpCzgSgps48eGQXt\n",
    "mzH76LFxG6FjmorukZ1Qou2hdT6wKxSNHht5RIs8NainOnDkplVAh1T3T9Axbw/dHlg+RbeMfvIj28n20KpGysEjBcbHLstk9YpO\n",
    "nNiQJYB+JHqApLS1bUdT9bHVNw3bCHSZgjVZDiJpm/iv8k3bwscP6GcrJX+XD239ynoKhn9q3j5g9/iraQwX679tgALmEunU6ZZj\n",
    "YNccOJikDXYnsG3eYvnS6HGSm3bBKlKD5YCSiGGlb570Ew+sih4pO9tA2cYJH1rPQ578KdPrq2c2B6rl8xUm9m9/C1QcUpaJPMWM\n",
    "fnJAFb7zJ3/XzxajrNWfPEptC3WPYVZglDxIbZOY5+zooCx5jErT5La52UaSVkPvc3i+cTEnj1BZnrh1vulvbA4e2zaOZQNmZUbJ\n",
    "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\n",
    "qWhMbohc9XDjykveq5Bahq5oTwriuDhFd9xV7TF7YYfboV9/wYXJM/DKukvtvES0Yu6QTmgW8+SJ3SqO+zhZEpFjj7FSDnRCskjz\n",
    "vR3zMnRCnJAC0faRk6/V8qzkNcz0oh73E9oEn/5HY8MnrInOljm7siJ2OmFNbIP9xQmxDeDCalnP6TrcEp7Zzg3J7L0onhUlE39n\n",
    "14WndEKa2GK4o4CY1Lg7dJtvj0e9bVstyfWoc90ke4nVI7pdREV9J51SlbhyvK/NAJ0QJsbOK0rTV+XVspz0PRyZSFoN69m0x6N8\n",
    "Sj2ynX9mx6IoOqFKEDFbhU9vV0ELnXElto5LxX5Y+U064Up0TZUiNUvxhIDU2fS5FG+9/6Q6ubCL6XO9p9UynTEZeWPXYPBjvmJq\n",
    "8XHBGqkTngRps1ddqmdEqWSzLRRPKWV9sQ6r/tDumxIzkMKshlBPHae0nnps/ZeVpnD61DwDNAKbSnS2mKMNcIVu+68/f/r5vz9/\n",
    "+eUfv37+189//PKPP//xyx9+/u2Xf/rlDz//26yvqNmq8emnk50SDz4HEnLvfxpnjGYrXDhIvP9lkjo0zJZx1Pj0uQqdCNi29em3\n",
    "M/v1ld2BNkeX978Vbl6dxtH0J/fH126I8lUE+Yse4vHukzk/oUt+OCp9+K2Q3/aSP6QGP/xWKNV7qSIBWfjxt12zqArQh9+NVZ+b\n",
    "AGAJ5xdjlZmLgcVZHPU+TW9gjJRKiwGc4yeTTIxGaR6yJiAhP70270AWsbf9ecnXPdrUSVIOnp9W3xAaVXas5aeXpvVSzbeO9GJm\n",
    "lW/rapjiC4zK++LTHEaOwR8XQWEHQBXOuDgsf/xx5OGKQE3jQP3xt5NNOUwtJ0fuj79tuytSXks4008m1XeakRmr/vzTounXDUjV\n",
    "b7Bm9bYDy/r/p7dBjVSiF2u+LauqmjZMz91N+847A4Pz2WnOtP/BqtX002evOQ9LvsaRhUHy9uNQ9e29ZcZdpMnk5d1v8xEkEBC8\n",
    "H4c5remFsWov3jv2/XYADvSz65y7FjroWxlI4U8/lY2ZY36H558OzoBBXkJI408hUZ+emiZmdCCRv3op54Gyfml6NuS2TSzX2GRN\n",
    "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\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",
    "теплее» знают все — нужны числа. В файле `data/weather.csv` — погода пяти городов за каждый день 2025 года.\n",
    "Новых методов в проекте нет."
   ],
   "id": "text-10"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Погода пяти городов\n",
    "import pandas as pd\n",
    "\n",
    "weather = pd.read_csv(\"data/weather.csv\", parse_dates=[\"date\"])\n",
    "weather.head(3)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "load"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 1. Подготовка\n",
    "\n",
    "В данных есть пропуски: датчики иногда не передавали показания. Отдельного столбца со средней температурой\n",
    "дня нет — посчитаем её как среднее между ночным минимумом и дневным максимумом."
   ],
   "id": "text-16"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 1. Пропуски и средняя температура дня\n",
    "\n",
    "Сохраните в `gaps` число пропусков в каждом столбце таблицы `weather`. Добавьте столбец `temp_avg` —\n",
    "полусумму `temp_min` и `temp_max`. В `no_avg` сохраните число пропусков в новом столбце.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`weather.isna().sum()`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "`(weather[\"temp_min\"] + weather[\"temp_max\"]) / 2` — скобки обязательны: деление выполняется раньше сложения.\n",
    "\n",
    "</details>"
   ],
   "id": "prep-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "gaps = ...\n",
    "# добавьте в weather столбец temp_avg\n",
    "no_avg = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "prep"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 1 — выполните после своего решения\n",
    "def test_gaps():\n",
    "    \"gaps — пропуски по столбцам\"\n",
    "    assert isinstance(gaps, pd.Series), f\"gaps — это {type(gaps).__name__}, а нужен Series\"\n",
    "    assert gaps[\"temp_min\"] == 3 and gaps[\"wind_ms\"] == 4 and gaps[\"date\"] == 0, \"числа не те: нужно число пропусков в каждом столбце\"\n",
    "\n",
    "\n",
    "def test_avg():\n",
    "    \"temp_avg — средняя температура дня\"\n",
    "    assert \"temp_avg\" in weather.columns, \"в weather нет столбца temp_avg\"\n",
    "    assert abs(weather.loc[0, \"temp_avg\"] - (-13.1)) > 1e-9, \"минимум и максимум сложены, но не разделены на 2: сумму возьмите в скобки\"\n",
    "    assert abs(weather.loc[0, \"temp_avg\"] - (-11.5)) > 1e-9, \"делится только максимум: сумму двух столбцов возьмите в скобки, потом делите на 2\"\n",
    "    assert abs(weather.loc[0, \"temp_avg\"] - (-6.55)) < 1e-9, f\"temp_avg в первой строке — {weather.loc[0, 'temp_avg']}: средняя — это сумма минимума и максимума, делённая на 2\"\n",
    "    assert no_avg == 5, f\"no_avg = {no_avg!r} — это не число пропусков в temp_avg\"\n",
    "\n",
    "_check(test_gaps, test_avg, names=[\"gaps\",\"no_avg\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "prep-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "gaps = weather.isna().sum()\n",
    "weather[\"temp_avg\"] = (weather[\"temp_min\"] + weather[\"temp_max\"]) / 2\n",
    "no_avg = weather[\"temp_avg\"].isna().sum()\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "prep-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*После решения упражнения выше:*\n",
    "\n",
    "Пропусков в `temp_avg` пять: три дня без минимума и два без максимума. Арифметика с пропуском даёт\n",
    "пропуск — и это честно: если одно из двух чисел неизвестно, неизвестно и их среднее. Пять дней из 1825 на итоги\n",
    "почти не влияют; оставим их пропусками — `mean` и `std` их не учитывают."
   ],
   "id": "component-30"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 2. Средняя температура и разброс"
   ],
   "id": "text-35"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 2. Два города\n",
    "\n",
    "Сохраните в `sochi` и `nsk` строки Сочи и Новосибирска — независимыми копиями (`.copy()`): дальше мы будем\n",
    "добавлять в них столбцы. По столбцу `temp_avg` посчитайте среднее (`sochi_mean`, `nsk_mean`) и стандартное\n",
    "отклонение (`sochi_std`, `nsk_std`).\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`weather[weather[\"city\"] == \"Сочи\"].copy()`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "`sochi[\"temp_avg\"].mean()` и `sochi[\"temp_avg\"].std()`.\n",
    "\n",
    "</details>"
   ],
   "id": "compare-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "sochi = ...\n",
    "nsk = ...\n",
    "sochi_mean = ...\n",
    "nsk_mean = ...\n",
    "sochi_std = ...\n",
    "nsk_std = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "compare"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 2 — выполните после своего решения\n",
    "def test_tables():\n",
    "    \"sochi и nsk — погода двух городов\"\n",
    "    assert isinstance(sochi, pd.DataFrame) and len(sochi) == 365 and (sochi[\"city\"] == \"Сочи\").all(), \"sochi — это должны быть 365 строк Сочи\"\n",
    "    assert isinstance(nsk, pd.DataFrame) and len(nsk) == 365 and (nsk[\"city\"] == \"Новосибирск\").all(), \"nsk — 365 строк Новосибирска\"\n",
    "    assert \"temp_avg\" in sochi.columns and \"temp_avg\" in nsk.columns, \"в таблицах нет столбца temp_avg: отбирайте строки из weather после шага 1\"\n",
    "\n",
    "\n",
    "def test_numbers():\n",
    "    \"средняя температура и её разброс\"\n",
    "    assert abs(sochi_mean - 14.9526) < 1e-3, f\"sochi_mean = {sochi_mean!r} — это не средняя температура в Сочи\"\n",
    "    assert abs(nsk_mean - 1.6303) < 1e-3, f\"nsk_mean = {nsk_mean!r} — это не средняя температура в Новосибирске\"\n",
    "    assert abs(sochi_std - 6.2736) < 1e-3, f\"sochi_std = {sochi_std!r} — это не стандартное отклонение temp_avg в Сочи\"\n",
    "    assert abs(nsk_std - 13.9545) < 1e-3, f\"nsk_std = {nsk_std!r} — это не стандартное отклонение temp_avg в Новосибирске\"\n",
    "\n",
    "_check(test_tables, test_numbers, names=[\"sochi\",\"nsk\",\"sochi_mean\",\"nsk_mean\",\"sochi_std\",\"nsk_std\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "compare-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "sochi = weather[weather[\"city\"] == \"Сочи\"].copy()\n",
    "nsk = weather[weather[\"city\"] == \"Новосибирск\"].copy()\n",
    "sochi_mean = sochi[\"temp_avg\"].mean()\n",
    "nsk_mean = nsk[\"temp_avg\"].mean()\n",
    "sochi_std = sochi[\"temp_avg\"].std()\n",
    "nsk_std = nsk[\"temp_avg\"].std()\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "compare-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*После решения упражнения выше:*\n",
    "\n",
    "Средняя температура года — 15 градусов против 1.6. Но различие не только в среднем: разброс в Новосибирске\n",
    "вдвое больше. Там и теплее, и холоднее, чем можно подумать, глядя на «плюс полтора в среднем»."
   ],
   "id": "component-47"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 3. Какие дни бывают\n",
    "\n",
    "Среднее скрывает, из чего оно сложилось. Разобьём дни по дневному максимуму на типы."
   ],
   "id": "text-51"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 3. Типы дней\n",
    "\n",
    "Добавьте в `nsk` и в `sochi` столбец `kind` — тип дня по `temp_max` с границами `bins` и подписями `names`.\n",
    "Сохраните в `nsk_kinds` и `sochi_kinds` доли дней каждого типа по порядку шкалы.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`pd.cut(nsk[\"temp_max\"], bins=bins, labels=names)`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "`nsk[\"kind\"].value_counts(normalize=True).sort_index()`.\n",
    "\n",
    "</details>"
   ],
   "id": "kinds-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "bins = [-50, 0, 15, 25, 50]\n",
    "names = [\"ниже нуля\", \"прохладно\", \"тепло\", \"жарко\"]\n",
    "# добавьте столбец kind в nsk и в sochi\n",
    "nsk_kinds = ...\n",
    "sochi_kinds = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "kinds"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 3 — выполните после своего решения\n",
    "def test_kind():\n",
    "    \"столбец kind в обеих таблицах\"\n",
    "    assert \"kind\" in nsk.columns and \"kind\" in sochi.columns, \"столбец kind нужен в обеих таблицах\"\n",
    "    assert (nsk[\"kind\"] == \"ниже нуля\").sum() == 148, \"в nsk типы дней не те: делить нужно столбец temp_max по границам bins\"\n",
    "    assert (sochi[\"kind\"] == \"жарко\").sum() == 50, \"в sochi типы дней не те: делить нужно столбец temp_max по границам bins\"\n",
    "\n",
    "\n",
    "def test_shares():\n",
    "    \"nsk_kinds и sochi_kinds — доли типов дней по порядку шкалы\"\n",
    "    for name, s in [(\"nsk_kinds\", nsk_kinds), (\"sochi_kinds\", sochi_kinds)]:\n",
    "        assert isinstance(s, pd.Series), f\"{name} — это {type(s).__name__}, а нужен Series\"\n",
    "        assert list(s.index) == [\"ниже нуля\", \"прохладно\", \"тепло\", \"жарко\"], f\"в {name} порядок {list(s.index)}, а нужен по шкале\"\n",
    "        assert abs(s.sum() - 1) < 1e-9, f\"в {name} числа дней, а нужны доли\"\n",
    "    assert abs(nsk_kinds[\"ниже нуля\"] - 148 / 365) < 1e-9, \"доли для Новосибирска не те\"\n",
    "    assert abs(sochi_kinds[\"жарко\"] - 50 / 365) < 1e-9, \"доли для Сочи не те\"\n",
    "\n",
    "_check(test_kind, test_shares, names=[\"nsk_kinds\",\"sochi_kinds\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "kinds-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "bins = [-50, 0, 15, 25, 50]\n",
    "names = [\"ниже нуля\", \"прохладно\", \"тепло\", \"жарко\"]\n",
    "nsk[\"kind\"] = pd.cut(nsk[\"temp_max\"], bins=bins, labels=names)\n",
    "sochi[\"kind\"] = pd.cut(sochi[\"temp_max\"], bins=bins, labels=names)\n",
    "nsk_kinds = nsk[\"kind\"].value_counts(normalize=True).sort_index()\n",
    "sochi_kinds = sochi[\"kind\"].value_counts(normalize=True).sort_index()\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "kinds-solution"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Доли типов дней: Новосибирск и Сочи\n",
    "print(nsk_kinds.round(2))\n",
    "print(sochi_kinds.round(2))"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "kinds-view"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*После решения упражнения выше:*\n",
    "\n",
    "В Новосибирске 41 % дней в году — с дневным максимумом ниже нуля. В Сочи таких дней нет вовсе, зато каждый\n",
    "седьмой день — жарче 25 градусов. При этом жаркие дни есть и в Новосибирске — 7 %."
   ],
   "id": "component-66"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 4. Осадки"
   ],
   "id": "text-70"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 4. Дождливо ли в Сочи\n",
    "\n",
    "Посчитайте для каждого города долю дней с осадками (`precip_mm` больше нуля) — `sochi_rainy` и `nsk_rainy` —\n",
    "и сумму осадков за год — `sochi_total` и `nsk_total`. В `sochi_heavy` сохраните число дней в Сочи, когда выпало\n",
    "10 мм и больше.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Доля — среднее маски `sochi[\"precip_mm\"] > 0`; сумма — `sochi[\"precip_mm\"].sum()`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "Число дней — сумма маски `sochi[\"precip_mm\"] >= 10`.\n",
    "\n",
    "</details>"
   ],
   "id": "rain-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "sochi_rainy = ...\n",
    "nsk_rainy = ...\n",
    "sochi_total = ...\n",
    "nsk_total = ...\n",
    "sochi_heavy = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "rain"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 4 — выполните после своего решения\n",
    "def test_rainy():\n",
    "    \"доля дней с осадками\"\n",
    "    assert abs(sochi_rainy - 110 / 365) < 1e-9, f\"sochi_rainy = {sochi_rainy!r} — это не доля дней с осадками в Сочи\"\n",
    "    assert abs(nsk_rainy - 145 / 365) < 1e-9, f\"nsk_rainy = {nsk_rainy!r} — это не доля дней с осадками в Новосибирске\"\n",
    "\n",
    "\n",
    "def test_totals():\n",
    "    \"сумма осадков за год и число дней с сильными осадками\"\n",
    "    assert abs(sochi_total - 795.0) < 1e-6, f\"sochi_total = {sochi_total!r} — это не сумма осадков в Сочи за год\"\n",
    "    assert abs(nsk_total - 520.5) < 1e-6, f\"nsk_total = {nsk_total!r} — это не сумма осадков в Новосибирске за год\"\n",
    "    assert sochi_heavy != 28, \"условие «10 мм и больше» включает и 10\"\n",
    "    assert sochi_heavy == 29, f\"sochi_heavy = {sochi_heavy!r} — это не число дней с осадками от 10 мм в Сочи\"\n",
    "\n",
    "_check(test_rainy, test_totals, names=[\"sochi_rainy\",\"nsk_rainy\",\"sochi_total\",\"nsk_total\",\"sochi_heavy\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "rain-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "sochi_rainy = (sochi[\"precip_mm\"] > 0).mean()\n",
    "nsk_rainy = (nsk[\"precip_mm\"] > 0).mean()\n",
    "sochi_total = sochi[\"precip_mm\"].sum()\n",
    "nsk_total = nsk[\"precip_mm\"].sum()\n",
    "sochi_heavy = (sochi[\"precip_mm\"] >= 10).sum()\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "rain-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*После решения упражнения выше:*\n",
    "\n",
    "Неожиданный результат: дней с осадками в Сочи **меньше** (30 % против 40 %), а воды выпадает в полтора раза\n",
    "больше. Дожди там реже, но сильнее: 29 дней в году — с осадками от 10 мм."
   ],
   "id": "component-82"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 5. Лето и зима\n",
    "\n",
    "**Годовая амплитуда** — разность средних температур самого тёплого и самого холодного месяца. Это главная\n",
    "характеристика континентальности климата."
   ],
   "id": "text-86"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 5. Амплитуда в Новосибирске\n",
    "\n",
    "Добавьте в `nsk` столбец `month` — номер месяца. Сохраните в `january` и `july` среднюю `temp_avg` января\n",
    "и июля, а в `amplitude` — их разность (июль минус январь).\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "`nsk[\"date\"].dt.month`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "`nsk.loc[nsk[\"month\"] == 1, \"temp_avg\"].mean()`.\n",
    "\n",
    "</details>"
   ],
   "id": "season-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# добавьте в nsk столбец month\n",
    "january = ...\n",
    "july = ...\n",
    "amplitude = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "season"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 5 — выполните после своего решения\n",
    "def test_month():\n",
    "    \"month — номер месяца\"\n",
    "    assert \"month\" in nsk.columns, \"в nsk нет столбца month\"\n",
    "    assert nsk[\"month\"].min() == 1 and nsk[\"month\"].max() == 12 and (nsk[\"month\"] == 2).sum() == 28, \"в month должны быть номера месяцев\"\n",
    "\n",
    "\n",
    "def test_amplitude():\n",
    "    \"january, july и amplitude — средняя температура января, июля и их разность\"\n",
    "    assert abs(january - (-17.554839)) < 1e-5, f\"january = {january!r} — это не средняя temp_avg января\"\n",
    "    assert abs(july - 22.546774) < 1e-5, f\"july = {july!r} — это не средняя temp_avg июля\"\n",
    "    assert amplitude > 0, \"amplitude получилась отрицательной: из июля вычитают январь\"\n",
    "    assert abs(amplitude - 40.101613) < 1e-5, f\"amplitude = {amplitude!r} — это не разность средних июля и января\"\n",
    "\n",
    "_check(test_month, test_amplitude, names=[\"january\",\"july\",\"amplitude\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "season-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "nsk[\"month\"] = nsk[\"date\"].dt.month\n",
    "january = nsk.loc[nsk[\"month\"] == 1, \"temp_avg\"].mean()\n",
    "july = nsk.loc[nsk[\"month\"] == 7, \"temp_avg\"].mean()\n",
    "amplitude = july - january\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "season-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 6. Оттепели в Москве\n",
    "\n",
    "Напоследок — вопрос о необычных днях. Оттепель — зимний день, когда дневная температура поднимается выше нуля."
   ],
   "id": "text-99"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 6. Зимние оттепели\n",
    "\n",
    "Сохраните в `winter` зимние дни таблицы `moscow` — декабрь, январь и февраль. В `thaws` — зимние дни\n",
    "с `temp_max` выше нуля: только столбцы `date` и `temp_max`. В `warmest_date` — дату самого тёплого зимнего дня.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Месяц из списка: `moscow[\"date\"].dt.month.isin([12, 1, 2])`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "`winter.loc[winter[\"temp_max\"] > 0, [\"date\", \"temp_max\"]]`; дата рекорда — `winter.loc[winter[\"temp_max\"].idxmax(), \"date\"]`.\n",
    "\n",
    "</details>"
   ],
   "id": "thaw-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "moscow = weather[weather[\"city\"] == \"Москва\"]\n",
    "winter = ...\n",
    "thaws = ...\n",
    "warmest_date = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "thaw"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 6 — выполните после своего решения\n",
    "def test_winter():\n",
    "    \"winter — зимние дни в Москве\"\n",
    "    assert isinstance(winter, pd.DataFrame), f\"winter — это {type(winter).__name__}, а нужна таблица\"\n",
    "    assert len(winter) == 90, f\"в winter {len(winter)} строк, а зимних дней (декабрь, январь, февраль) 90\"\n",
    "    assert set(winter[\"date\"].dt.month) == {12, 1, 2}, \"в winter должны быть только декабрь, январь и февраль\"\n",
    "\n",
    "\n",
    "def test_thaws():\n",
    "    \"thaws — оттепели, warmest_date — самый тёплый зимний день\"\n",
    "    assert isinstance(thaws, pd.DataFrame), f\"thaws — это {type(thaws).__name__}, а нужна таблица\"\n",
    "    assert list(thaws.columns) == [\"date\", \"temp_max\"], f\"столбцы thaws сейчас {list(thaws.columns)}, а нужны date и temp_max\"\n",
    "    assert len(thaws) == 5 and thaws[\"temp_max\"].min() > 0, f\"в thaws {len(thaws)} строк — проверьте условие: максимум выше нуля\"\n",
    "    assert not isinstance(warmest_date, pd.Series), \"warmest_date — целая строка, а нужна одна дата\"\n",
    "    assert warmest_date == pd.to_datetime(\"2025-12-03\"), f\"warmest_date = {warmest_date} — это не дата самого тёплого зимнего дня\"\n",
    "\n",
    "_check(test_winter, test_thaws, names=[\"winter\",\"thaws\",\"warmest_date\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "thaw-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "moscow = weather[weather[\"city\"] == \"Москва\"]\n",
    "winter = moscow[moscow[\"date\"].dt.month.isin([12, 1, 2])]\n",
    "thaws = winter.loc[winter[\"temp_max\"] > 0, [\"date\", \"temp_max\"]]\n",
    "warmest_date = winter.loc[winter[\"temp_max\"].idxmax(), \"date\"]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "thaw-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Справка"
   ],
   "id": "text-111"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Итог\n",
    "print(\"Средняя температура: Сочи\", round(sochi_mean, 1), \"· Новосибирск\", round(nsk_mean, 1))\n",
    "print(\"Разброс (std): Сочи\", round(sochi_std, 1), \"· Новосибирск\", round(nsk_std, 1))\n",
    "print(\"Осадки за год, мм: Сочи\", round(sochi_total), \"· Новосибирск\", round(nsk_total))\n",
    "print(\"Дней с осадками: Сочи\", round(sochi_rainy * 100), \"% · Новосибирск\", round(nsk_rainy * 100), \"%\")\n",
    "print(\"Годовая амплитуда в Новосибирске:\", round(amplitude, 1), \"градуса\")\n",
    "print(\"Оттепелей в Москве за зимние месяцы:\", len(thaws))"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "summary"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выводы"
   ],
   "id": "text-115"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*После решения всех упражнений урока:*\n",
    "\n",
    "- В Сочи в среднем на 13 градусов теплее, и температура вдвое стабильнее.\n",
    "- В Новосибирске 41 % дней — с морозом даже днём; в Сочи таких дней нет.\n",
    "- Осадков в Сочи больше в полтора раза, хотя дождливых дней меньше: дожди реже, но сильнее.\n",
    "- Годовая амплитуда в Новосибирске — 40 градусов: от −17.6 в январе до +22.5 в июле.\n",
    "- В Москве за три зимних месяца 2025 года (январь, февраль и декабрь) было пять оттепелей — 6 % зимних дней."
   ],
   "id": "component-118"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Мы сравнили два города — и для каждого писали одни и те же строки дважды. Городов в файле пять; для пяти\n",
    "пришлось бы писать впятеро больше. Следующий модуль — о том, как посчитать всё это по каждому городу одной\n",
    "командой."
   ],
   "id": "text-125"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Подробнее в справочнике:** [pandas/stats](https://ds-edu-trainer.vercel.app/reference/pandas/stats) · [pandas/cut-qcut](https://ds-edu-trainer.vercel.app/reference/pandas/cut-qcut) · [pandas/datetime](https://ds-edu-trainer.vercel.app/reference/pandas/datetime) · [pandas/missing](https://ds-edu-trainer.vercel.app/reference/pandas/missing)"
   ],
   "id": "more"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
