Shanraq.org Shanraq.org
Внешнее и своё: один шок — разные цены
IT

Python: от данных до своей сводки Урок 43 из 56

Внешнее и своё: один шок — разные цены

Сорок второй урок курса по Python. В 2022 году мировая еда подорожала на 14.9 % — одинаково для всех. Инфляция у десяти соседей вышла от 8.6 до 72.3 %, разница в восемь раз. Две привычные причины — курс и деньги — проверяются связью, и одна страна двигает ответ, а у денег ещё и переворачивает знак.

Зачем это нужно

Когда цены растут, объяснение находится за секунду и всегда одно из двух: «виноват мир» или «виноваты свои». Обе фразы проверяемы, и проверка стоит одного вечера.

Мир в 2022 году был у всех один и тот же: индекс мировых цен на еду вырос на 14.9 % — не «примерно», а буквально одно число для всех стран сразу. Инфляция при этом разошлась в восемь раз. Значит, одним внешним шоком дело не объясняется, и надо смотреть, чем страны отличались.

Заодно урок вводит меру, которая понадобится дальше: связь двух столбцов и вопрос, сколько наблюдений за ней стоит.

Сразу целиком

Файл shok.py. Мировые цены — индекс ФАО, 2014–2016 = 100. Инфляция, курс и денежная масса — Всемирный банк за 2022 год.

"""Урок 42: внешнее и своё — один шок, разные цены.

Все числа настоящие. Мировые цены на еду — индекс ФАО, 2014–2016 = 100.
Инфляция, курс к доллару и денежная масса — Всемирный банк, 2022 год,
показатели FP.CPI.TOTL.ZG, PA.NUS.FCRF и FM.LBL.BMNY.CN.
"""

import pandas as pd

# Мировые цены на еду: один и тот же ряд для всех стран сразу.
FAO = pd.Series({2019: 94.93, 2020: 98.05, 2021: 125.73, 2022: 144.51, 2023: 124.52})

# Десять соседей в год шока: инфляция, изменение курса к доллару и рост
# денежной массы, всё в процентах за 2022 год. Пропуск — это пропуск: у двух
# стран Всемирный банк не публикует денежную массу за эти годы.
SOSEDI = pd.DataFrame(
    [
        ("Армения", 8.64, -13.52, 16.10),
        ("Узбекистан", 11.45, 4.15, 30.17),
        ("Грузия", 11.90, -9.48, 11.04),
        ("Россия", 13.74, -7.02, None),
        ("Азербайджан", 13.85, 0.00, 23.60),
        ("Кыргызстан", 13.92, -0.62, 30.59),
        ("Казахстан", 15.03, 8.04, 13.94),
        ("Беларусь", 15.21, 3.44, None),
        ("Молдова", 28.74, 6.88, 5.27),
        ("Турция", 72.31, 86.98, 60.34),
    ],
    columns=["страна", "инфляция", "курс", "деньги"],
).set_index("страна")

print("== Один шок на всех")
world = FAO.pct_change().mul(100).round(1)
print(pd.DataFrame({"индекс ФАО": FAO, "за год, %": world}).to_string())
print("  в 2022 году мировая еда подорожала на", world[2022], "% — одинаково для всех десяти")

print()
print("== Десять соседей, один год")
print(SOSEDI["инфляция"].sort_values().to_string())
low, high = SOSEDI["инфляция"].min(), SOSEDI["инфляция"].max()
print("  от", low, "до", high, "— разница в", round(high / low, 1), "раза")
print("  медиана:", round(SOSEDI["инфляция"].median(), 2))

print()
print("== Два объяснения, которые обычно называют")
for column, name in (("курс", "свой курс к доллару"), ("деньги", "своя денежная масса")):
    pair = SOSEDI[["инфляция", column]].dropna()
    print(f"  {name:22} связь {SOSEDI['инфляция'].corr(SOSEDI[column]):+.3f}"
          f"   стран в счёте: {len(pair)}")

print()
print("== Что с этими связями делает одна страна")
bez = SOSEDI.drop("Турция")
print(f"  {'':22} {'все десять':>12} {'без Турции':>12}")
for column, name in (("курс", "курс"), ("деньги", "деньги")):
    a = SOSEDI["инфляция"].corr(SOSEDI[column])
    b = bez["инфляция"].corr(bez[column])
    print(f"  {name:22} {a:>12.3f} {b:>12.3f}")

print()
print("== Кого не объясняет ни одно")
miss = SOSEDI.loc[["Молдова", "Кыргызстан"]]
print(miss.to_string())
print("  Молдова: цены +28.7 % при курсе +6.9 % и деньгах +5.3 %")
print("  Кыргызстан: деньги +30.6 %, а цены выросли как у соседей с +14 %")

Вывод:

== Один шок на всех
      индекс ФАО  за год, %
2019       94.93        NaN
2020       98.05        3.3
2021      125.73       28.2
2022      144.51       14.9
2023      124.52      -13.8
  в 2022 году мировая еда подорожала на 14.9 % — одинаково для всех десяти

== Десять соседей, один год
страна
Армения         8.64
Узбекистан     11.45
Грузия         11.90
Россия         13.74
Азербайджан    13.85
Кыргызстан     13.92
Казахстан      15.03
Беларусь       15.21
Молдова        28.74
Турция         72.31
  от 8.64 до 72.31 — разница в 8.4 раза
  медиана: 13.88

== Два объяснения, которые обычно называют
  свой курс к доллару    связь +0.971   стран в счёте: 10
  своя денежная масса    связь +0.739   стран в счёте: 8

== Что с этими связями делает одна страна
                           все десять   без Турции
  курс                          0.971        0.597
  деньги                        0.739       -0.538

== Кого не объясняет ни одно
            инфляция  курс  деньги
страна                            
Молдова        28.74  6.88    5.27
Кыргызстан     13.92 -0.62   30.59
  Молдова: цены +28.7 % при курсе +6.9 % и деньгах +5.3 %
  Кыргызстан: деньги +30.6 %, а цены выросли как у соседей с +14 %

Разбор

Один шок действительно один

2021      125.73      28.2
2022      144.51      14.9

Индекс ФАО — это мировые цены на еду: пшеница, масло, сахар, молоко, мясо. Он не про страну, он про мир, и в 2022 году он один и тот же для Армении и для Турции.

Поэтому фраза «цены выросли из-за мировых цен» проверяема: общий внешний шок должен был затронуть всех, но его сила зависит от доли импорта, курса, корзины и того, как цены переносятся внутрь страны. Инфляция разошлась от 8.64 до 72.31 — в 8.4 раза. Значит, одного общего фактора для объяснения различий недостаточно.

Связь: одно число вместо десяти сравнений

Series.corr считает коэффициент корреляции Пирсона. +1 означает идеальную положительную линейную связь, −1 — идеальную отрицательную, 0 — отсутствие линейной связи, но не любой возможной связи. Здесь сравниваются различия между странами в один год, а не движение двух рядов во времени. Это не «во сколько раз» и не «на сколько процентов».

  свой курс к доллару    связь +0.971   стран в счёте: 10
  своя денежная масса    связь +0.739   стран в счёте: 8

Первое число выглядит как закон природы. Второе — как слабое подтверждение. И рядом стоит то, без чего оба бессмысленны: сколько стран за ними. У денежной массы — восемь, а не десять: у России и Беларуси Всемирный банк её за эти годы не публикует, и pandas молча выбросил эти две пары. Число наблюдений всегда печатают рядом со связью.

Одна страна решает ответ

  курс                          0.971        0.597
  деньги                        0.739       -0.538

Уберите Турцию — одну строку из десяти, — и «закон природы» превращается в намёк, а связь с деньгами меняет знак: была положительная, стала отрицательная. То есть на девяти странах данные говорят «где денег стало больше, там цены росли медленнее», что как утверждение о мире звучит дико — и правильный вывод здесь не «деньги не важны», а «на десяти точках такой вопрос не решается».

Это та же история, что в тридцать седьмом уроке про среднее: там одно значение переписывало среднее, здесь одно значение переписывает связь. Разница в том, что связь выглядит убедительнее — у неё нет единиц измерения, и её легко принять за вывод.

Кого не объясняет ни одно из двух

Молдова        28.74  6.88    5.27
Кыргызстан     13.92 -0.62   30.59

Строка Молдовы показывает: при росте денежной массы на 5.3 % и изменении курса на 6.9 % инфляция составила 28.7 %. Это может отражать другие факторы, например энергетический шок, но по этой таблице причину установить нельзя. Кыргызстан показывает обратное: денежная масса выросла на 30.6 %, а инфляция осталась близкой к уровню соседей.

Пара таких строк — лучшая проверка объяснения. Если причина названа верно, она должна работать и на них.

Почему нельзя сказать «виноват курс»

Связь +0.971 — не доказательство направления. Девальвация поднимает цены на импорт, но и высокая инфляция роняет курс, и обе они могут идти от третьей причины, которой нет в таблице. Связь говорит только: «эти два столбца двигались вместе». Кто кого двигал, из неё не следует, и никакая величина связи этого не изменит.

Практическое правило: связь — это повод искать механизм, а не заменитель механизма.

Сколько нужно точек

Прямого ответа нет, но есть проверка, которая честнее любого правила: посчитать связь без каждой страны по очереди. Если один ряд двигает ответ на треть, а другой переворачивает знак, точек мало — независимо от того, десять их или сто. Эта проверка и стоит в задании.

Карта урока

Карта урока: один шок, десять стран, одна связь

Скажите своими словами

Ответьте вслух или на бумаге, не подглядывая. Ответы — в конце урока.

  1. Почему одинаковый для всех мировой шок не объясняет разницу в восемь раз?
  2. Что означает связь +0.971 и чего она не означает?
  3. Зачем рядом со связью печатать число наблюдений?

Разминка

Три коротких шага перед заданием: предсказать, дополнить, починить. Ответы — в конце урока, но сначала ответьте сами.

1. Предскажите. Что напечатает программа и почему второе число почти единица?

import pandas as pd

a = pd.Series([1, 2, 3, 4])
b = pd.Series([4, 3, 2, 1])
print("связь:", round(a.corr(b), 3))
a_plus = pd.Series([1, 2, 3, 4, 100])
b_plus = pd.Series([4, 3, 2, 1, 100])
print("и с одной большой парой:", round(a_plus.corr(b_plus), 3))

2. Заполните пропуск. Вместо ... посчитайте, сколько стран реально участвует в связи.

# инфляция есть у всех, денежная масса — не у всех
import pandas as pd

table = pd.DataFrame(
    {"инфляция": [8.64, 11.45, 13.74, 15.21], "деньги": [16.10, 30.17, None, None]},
    index=["Армения", "Узбекистан", "Россия", "Беларусь"],
)
pair = ...
print("стран в счёте:", len(pair))

3. Почините. Программа печатает связь и утверждает, что она посчитана по четырём странам.

# у двух стран денежной массы нет, и pandas молча выбрасывает эти пары
import pandas as pd

table = pd.DataFrame(
    {"инфляция": [8.64, 11.45, 13.74, 15.21], "деньги": [16.10, 30.17, None, None]},
    index=["Армения", "Узбекистан", "Россия", "Беларусь"],
)
print("связь:", round(table["инфляция"].corr(table["деньги"]), 3), "по", len(table), "странам")

Задание

Обязательное. По таблице десяти соседей посчитайте связь инфляции с курсом и с денежной массой, печатая рядом число стран. Затем пересчитайте обе связи, убирая по одной стране, и напечатайте таблицу: связь без страны и сдвиг относительно полной. Отсортируйте по величине сдвига. В конце назовите страну, которая двигает ответ сильнее всех, и отдельно — те, из-за которых связь меняет знак.

Ожидаемый вывод:

== Связь и сколько стран за ней стоит
  курс     +0.971  по 10 странам
  деньги   +0.739  по 8 странам

== Если убрать по одной стране
              курс  деньги  курс, сдвиг  деньги, сдвиг
Турция       0.597  -0.538       -0.374         -1.277
Молдова      0.985   0.891        0.014          0.152
Узбекистан   0.978   0.793        0.007          0.054
Казахстан    0.977   0.735        0.006         -0.004
Грузия       0.972   0.728        0.001         -0.011
Россия       0.972   0.739        0.001          0.000
Беларусь     0.972   0.739        0.001          0.000
Армения      0.971   0.728        0.000         -0.011
Азербайджан  0.971   0.747        0.000          0.008
Кыргызстан   0.971   0.782        0.000          0.043

== Кто решает ответ
  курс     без страны «Турция» связь идёт с +0.971 на +0.597
  деньги   без страны «Турция» связь идёт с +0.739 на -0.538

== Проверка
  курс     знак меняют страны: никакие
  деньги   знак меняют страны: Турция

Готово — это: вывод совпадает построчно; число стран печатается рядом с каждой связью, а не один раз внизу; «кто решает ответ» находится по величине сдвига, а не глазами; смена знака проверяется сравнением знаков, а не на глаз; сортировка устойчивая — сдвиги совпадают у нескольких стран, и обычная сортировка расставит их на разных машинах по-разному.

На своих данных. Возьмите любые два столбца, которые, как вам кажется, связаны: расходы и погоду, посещаемость и день недели, продажи и курс. Посчитайте связь, посчитайте её без каждой точки по очереди — и посмотрите, сколько точек нужно убрать, чтобы ответ изменился. Это и есть мера того, насколько вы можете на него опираться.

По желанию.

  • Посчитайте связь инфляции с курсом на логарифмах (numpy.log1p) и посмотрите, насколько меньше её двигает Турция.
  • Добавьте одиннадцатую страну со средними значениями и проверьте, что связь почти не изменилась: устойчивость проверяется добавлением, а не только удалением.
  • Сравните corr() по умолчанию (Пирсон) и corr(method="spearman") — второй считает по рангам, и выброс двигает его меньше.

Куда это встанет в проекте

Шаг двадцать первый, и он про то, чего сводка делать не станет.

В сводке три страны. Вопрос «двигались ли цены и деньги вместе» на трёх странах даёт число всегда — и это число ничего не стоит, что урок только что и показал на десяти. Поэтому esep.baylanys получает порог: считает страны, у которых есть оба числа, сравнивает с порогом и возвращает либо связь с числом стран, либо отказ и то, чего не хватило.

На живых данных выходит отказ, и даже дважды заслуженный: аз: 2 ел, 8 керек — у России Всемирный банк не публикует денежную массу, так что из трёх стран остаются две. Отказ печатается на странице отдельной строкой: строка, которая исчезает при нехватке данных, учит читателя, что она была необязательной.

Порог — единственное число в esep.py, которое выбрано, а не измерено: восемь мало настолько, чтобы региональная сводка до него дотянулась, и велико настолько, чтобы знак не решала одна страна.

Ответы

Показать ответы

На вопросы

  1. Потому что шок у всех одинаковый, а результат разошёлся в 8.4 раза. Одинаковая причина не может объяснить разный результат — значит, работает что-то ещё, и это «ещё» у каждой страны своё.
  2. Что два столбца двигались вместе: где курс упал сильнее, там и цены выросли сильнее. Она не означает, что курс — причина, не означает «на столько-то процентов» и не переносится на страну, которой нет в таблице.
  3. Потому что pandas молча выбрасывает пары с пропуском: те же +0.739 посчитаны по восьми странам, а не по десяти. Связь без числа наблюдений — половина утверждения, и обычно та половина, которая красивее.

К разминке

  1. Четыре точки лежат на идеальной убывающей прямой, поэтому связь ровно −1. Одна пара из двух больших чисел добавляет общее движение вверх, и та же самая четвёрка становится почти идеальной прямой, только растущей.
связь: -1.0
и с одной большой парой: 0.999
  1. table[["инфляция", "деньги"]].dropna(). Считать надо пары, а не строки: страна без денежной массы в связь не входит.
# инфляция есть у всех, денежная масса — не у всех
import pandas as pd

table = pd.DataFrame(
    {"инфляция": [8.64, 11.45, 13.74, 15.21], "деньги": [16.10, 30.17, None, None]},
    index=["Армения", "Узбекистан", "Россия", "Беларусь"],
)
pair = table[["инфляция", "деньги"]].dropna()
print("стран в счёте:", len(pair))
стран в счёте: 2
  1. Само число связи pandas считает правильно — по двум парам. Неправдой было предложение вокруг него: «по четырём странам».
# та же таблица: связь печатают вместе с числом стран, на которых она посчитана
import pandas as pd

table = pd.DataFrame(
    {"инфляция": [8.64, 11.45, 13.74, 15.21], "деньги": [16.10, 30.17, None, None]},
    index=["Армения", "Узбекистан", "Россия", "Беларусь"],
)
pair = table[["инфляция", "деньги"]].dropna()
print("связь:", round(pair["инфляция"].corr(pair["деньги"]), 3), "по", len(pair), "странам")
связь: 1.0 по 2 странам

И заодно то, ради чего эта разминка написана: связь по двум точкам равна единице всегда. Через две точки проходит ровно одна прямая, так что единица здесь — не результат, а предупреждение.

К заданию

Сдвиги выстраиваются в лестницу: Турция двигает связь с курсом на 0.374, а с деньгами — на 1.277, следующая за ней Молдова — на 0.014 и 0.152. Порядок в сто раз. Когда одно наблюдение так отрывается от остальных, ответ принадлежит ему, а не данным.

Проверка знака важнее величины. Связь, которая из положительной становится отрицательной при удалении одной строки, не «слабая» — она не определена на этих данных, и единственный честный вывод из неё: нужно больше стран или другой вопрос.

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.