
Утро вторника, чашка кофе, интенсивная борьба с выгоранием, бумажная работа, на глаза попадается датасет с данными по COVID-19. «Ого, это интересная штука, много цифр по какой-то тематике, что с этим обычно делают?», подумал я.
Поисковик предложил эти данные проанализировать, раз я не программист, то анализировать питоном, раз я не разбираюсь в теории вероятности, то законом Бэнфорда. Любопытство: «Ок, бро, ты в деле!».
Закон Бэнфорда подошел мне по нескольким причинам. Датасет имеет числа не ограниченные диапазонами значений, достаточно большой, содержит цифры и эпидемии имеют экспоненциальный рост.
Python подошел мне по нескольким причинам. Я люблю змей. Два года пытался его изучать в теории, но оказалось это совершенно не нужно, достаточно просто начать программировать на нем.
Датасет представлял собой три варианта хранения, я выбрал json, потому что мне нравится как это звучит и я слышал, что это крутая и опасная штука в умелых руках.
Для начала я сдернул 24 вкуснейших мегабайта к себе на диск, чтобы на время тестов не дергать каждый раз это с сервера:
import json
import urllib.request
def get_json(url_data):
web_url = urllib.request.urlopen(url_data)
data = web_url.read()
data = json.loads(data)
return data
def corona_get():
url = 'https://covid.ourworldindata.org/data/owid-covid-data.json'
json_data = get_json(url)
with open('data.json', 'w') as f:
json.dump(json_data, f)
Затем, как фильме Нолана «Начало» я спускался все глубже в недра json, пока не достиг нужных данных total_cases. Как оказалось, датасет имеет не очень обязательную структуру.
Во избежание исключений я сделал проверку через try-except собирая число ошибок, для определения погрешности (на самом деле было бы неплохо где-то выводить число ошибок на срез, но меня захватила сама идея и я просто заленился).
Затем перешел непосредственно к алгоритму определения частоты появления цифры.
# Роемся в данных на регион, при желании можно проверять документ не вдоль, а поперек, делая срезы по датам, но об этом позже.
for e in json_data[region]['data']:
# Так я удумал проверять наличие значения в документе json.
def data_cases_exist():
try:
test = e['total_cases']
return True
except:
return False
# Если значение существует, таки добавляет его в список.
if data_cases_exist():
list_cases.append(int(e['total_cases']))
# Иначе считает сколько таких ошибок на срез, неплохо бы такое знать, вообще-то.
else:
error_count += 1
Уберите ребенка от экрана, потому что далее идет код, требующий еще большего рефакторинга, так как частично является заимствованием с неуклюжей доработкой.
В нем я создаю отсортированный для распределения по закону Бэнфорда словарь, где ключом является первый разряд числа, а значением частота его появления.
# умные люди говорят, что с коллекциями проще, чем со словарями.
import collections
# Делает лист benford_list из первых цифр значений
for i in list(map(str, list_cases)):
benford_list.append(i[0])
# Тут питон делает подсчет частоты появления цифры в benford_list
results = collections.Counter()
for i in benford_list:
results1 = collections.Counter(str(i))
results = results + results1
# Сортировка коллекции в порядке убывания по ключам, словарь якобы так красиво не умеет. "Подписывайесь на канал, ставьте лайки и оставляйте коментарий так ли это."
dict_b = dict(results.most_common(10))
# Тут я вроде бы пытался конвертировать строковые ключи в целое, зачем, уже не помню сам.
dict_b = {int(k): int(v) for k, v in dict_b.items()}
# Сортровка по ключам от 1 до 9
sorted_dict = dict(sorted(dict_b.items()))
# Ноли нам не понадобятся, поэтому убираем ноль из нашего словаря
sorted_dict.pop(0)
return sorted_dict
И вот, наслаждаясь своей работой, я держу в руках голубоватый, переливающийся на свету отсортированный sorted_dict региона RUS.
{1: 14559, 2: 10128, 3: 3798, 4: 4431, 5: 3587, 6: 4009, 7: 3798, 8: 4853, 9: 4853}
Показываю друзьям. Кидаюсь ссылкой на numberphile о законе Бэнфорда. Рассказываю про эпидемии… Что ж, хвастаться числовыми словарями перед друзьями получилось не очень, придется визуализировать красивыми гистограммами.
Окей гугл, чем строят графики хорошие мальчики в 2020? Гугл голосом {username} отвечает: «Есть одна очень крутая штука, может всё, если хорошо разберешься». Если хорошо разберешься… Challenge accepted!
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
# Передаем наш словарь и наименование региона, чтобы сразу понимать, что на гистограмме.
def benford_plot(sorted_dict, region):
# x это наши цифры от 1 до 9
x = []
# y1 это частота появления цифры
y1 = []
# y2 это список по которому мы будем строить кривую по закону Бэнфорда для сравнения.
# Насколько я понимаю трактовку закона, танцевать нужно от 1, либо... нуакак?
y2 = [sorted_dict[1]]
# Строим остальные 8 столбцов по закону Бэнфорда, первый, по идее, это максимум.
for i in range(8):
# Каждый следующий стобец ниже на ben_percent
ben_percent = math.log10(1 + (1 / (i+1)))
value = y2[i] - (y2[i] * ben_percent)
y2.append(value)
# Наполняем листы для отображения осей координат
for key, value in sorted_dict.items():
x.append(key)
y1.append(value)
fig, ax = plt.subplots()
# Гистограмма по данным из датасета
ax.bar(x, y1, color=[0, 0, 0], width=0.5)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
# Гистограмма по мнению товарища Бэнфорда
ax.bar(x, y2, color=[0.4, 0.4, 0.4, 0.35415316])
# Расставляет красиво значения над столбиками
for x, y in zip(x, y1):
plt.text(x, y + 0.05, '%d' % y, ha='center', va='bottom')
fig.set_figwidth(12)
fig.set_figheight(6)
fig.set_facecolor('floralwhite')
ax.set_facecolor('seashell')
plt.title('Total cases in ' + region + ' for all time', fontdict=None, loc='center')
plt.savefig('pic\\' + region + '.png')
plt.close()
Таким образом мы сложили в pic гистограмму с регионом и если определить все это в цикл, перебирающий в json все регионы, то получим еще больше красивых картинок. Поэтому:
if __name__ == '__main__':
f = open(r'data.json')
json_data = json.load(f)
for i in json_data:
location = json_data[i]['location']
benford_plot(benford_data(i), location)
Для проверки я использовал генератор случайных чисел и ряд Фибоначчи, потому что первый есть хаос, а второй есть экспонента, ровно как и рост заболеваемости при эпидемиях.
Иными словами ГСЧ не впишется в закон Бэнфорда, а ряд Фибоначчи и все графики с датасета впишутся идеально.
Для контроля статистической значимости я использовал Z-тест, ведя расчет по формуле: $inline$\frac {|AP - EP|-\frac{1}{2N}} { \sqrt {\frac { EP(1-EP)} {N} }}$inline$, взятой из нескольких источников, например отсюда или отсюда, где AP вероятность распределения из датасета, EP теоретическая вероятность по Бэнфорду, N число иcследуемых элементов. Исходя из доверительной вероятности в 1,96 на графиках внизу столбца Z < 1,96 выводил белым, Z > 1,96 оранжевым и Z > 10 красным. Запутались? Я запутался, если вы понимаете какие выводы можно сделать из этих значений, пишите, попробуем разобраться. Насколько я понимаю, при достаточно больших данных такое возможно, что z-stest показывает все значения как статистически значимые и принимать во внимание нужно действительно высокие показатели Z, именно поэтому я выбрал градацию цветов с оранжевым и красным. Говоря проще, столбцы с оранжевыми показателями настораживают, а с красными вызывают явные подозрения в нарушении закона Бэнфорда.
Закон Бэнфорда и ряд Фибоначчи (50000 чисел):

Закон Бэнфорда и случайные числа (50000 чисел):

Выборочные данные из датасета
Аргентина вроде даже вписывается.

Самая чистая с точки зрения Z-теста с большим отрывом Украина

Гватемала не вписывается и скорее похожа на случайные числа.

Ну и еще несколько примеров. Россия.

Словакия.

Словения.

Швеция.


Самая чистая с точки зрения Z-теста с большим отрывом Украина

Гватемала не вписывается и скорее похожа на случайные числа.

Ну и еще несколько примеров. Россия.

Словакия.

Словения.

Швеция.

Такие вот интересные результаты. За время написания материала я нашел ошибку в алгоритме, переписал код, нашел ошибку в коде, опять переписал, а еще меня посетило много мыслей, которые по-хорошему должны посещать до написания статьи. И я подумал, а может я где-то ошибся? Для чистоты эксперимента я попробую найти датасет по эпидемиям, данные о которых с меньшей вероятностью кто-то мог пытаться фальсифицировать, например туберкулёз, но на поверку такие данные найти не так и просто.
Больше гистограмм




























































































































































































































































































































































Вдохновлялся Testing Benford’s law.
Данные c ourworldindata.org.