Search
Write a publication
Pull to refresh

Закон Бэнфорда и COVID-19

image

Утро вторника, чашка кофе, интенсивная борьба с выгоранием, бумажная работа, на глаза попадается датасет с данными по 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-теста с большим отрывом Украина



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



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



Словакия.



Словения.



Швеция.




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

Больше гистограмм




























































































































































































































































































































































Вдохновлялся Testing Benford’s law.

Данные c ourworldindata.org.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.