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 — не доказательство направления. Девальвация поднимает цены на импорт, но и высокая инфляция роняет курс, и обе они могут идти от третьей причины, которой нет в таблице. Связь говорит только: «эти два столбца двигались вместе». Кто кого двигал, из неё не следует, и никакая величина связи этого не изменит.
Практическое правило: связь — это повод искать механизм, а не заменитель механизма.
Сколько нужно точек
Прямого ответа нет, но есть проверка, которая честнее любого правила: посчитать связь без каждой страны по очереди. Если один ряд двигает ответ на треть, а другой переворачивает знак, точек мало — независимо от того, десять их или сто. Эта проверка и стоит в задании.
Карта урока
Скажите своими словами
Ответьте вслух или на бумаге, не подглядывая. Ответы — в конце урока.
- Почему одинаковый для всех мировой шок не объясняет разницу в восемь раз?
- Что означает связь +0.971 и чего она не означает?
- Зачем рядом со связью печатать число наблюдений?
Разминка
Три коротких шага перед заданием: предсказать, дополнить, починить. Ответы — в конце урока, но сначала ответьте сами.
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, которое выбрано, а не измерено: восемь мало настолько, чтобы региональная сводка до него дотянулась, и велико настолько, чтобы знак не решала одна страна.
Ответы
Показать ответы
На вопросы
- Потому что шок у всех одинаковый, а результат разошёлся в 8.4 раза. Одинаковая причина не может объяснить разный результат — значит, работает что-то ещё, и это «ещё» у каждой страны своё.
- Что два столбца двигались вместе: где курс упал сильнее, там и цены выросли сильнее. Она не означает, что курс — причина, не означает «на столько-то процентов» и не переносится на страну, которой нет в таблице.
- Потому что pandas молча выбрасывает пары с пропуском: те же +0.739 посчитаны по восьми странам, а не по десяти. Связь без числа наблюдений — половина утверждения, и обычно та половина, которая красивее.
К разминке
- Четыре точки лежат на идеальной убывающей прямой, поэтому связь ровно −1. Одна пара из двух больших чисел добавляет общее движение вверх, и та же самая четвёрка становится почти идеальной прямой, только растущей.
связь: -1.0
и с одной большой парой: 0.999
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
- Само число связи 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. Порядок в сто раз. Когда одно наблюдение так отрывается от остальных, ответ принадлежит ему, а не данным.
Проверка знака важнее величины. Связь, которая из положительной становится отрицательной при удалении одной строки, не «слабая» — она не определена на этих данных, и единственный честный вывод из неё: нужно больше стран или другой вопрос.
Источники
- Индекс продовольственных цен ФАО — мировые цены на еду, месяцы и годы, 2014–2016 = 100.
- Инфляция потребительских цен, Всемирный банк — инфляция десяти соседей за 2022 год.
- Официальный курс валюты, Всемирный банк — курс к доллару, из которого посчитана девальвация.
- Series.corr — как pandas считает связь и что делает с пропусками.
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.