Някъде в началото на това десетилетие ме хвана манията по настолните игри. Около всеки месец преглеждах „игрите с най-висок рейтинг“ на BoardGameGeek (BGG) за нови настолни игри, които да купя. Има буквално десетки хиляди настолни игри, изброени в BGG, много с рецензии и критики. Любимата ми стратегическа игра не е необичаен избор. По-скоро това в момента е оценената игра №4 за всички времена на BGG и бивша №1. Тази игра е „Борбата на здрача“.

Twilight Struggle е стратегическа игра за 2 играчи с карти на тема „Студена война“. По някакъв начин се усеща като комбинация от шах и покер. Едната страна играе със Съединените щати, а другата страна играе със Съветския съюз. Играта има същото параноично усещане като истинската Студена война, тъй като постоянно трябва да гадаете кои карти може да има опонентът ви и как може да ви навреди с тези карти.

Докато уменията са много важни в TS, късметът също играе важна роля в резултатите. Играта включва преврати, пренастройки, военни карти и космическа надпревара, всички от които се определят от хвърляния на зара.

Преди няколко години, след успешна кампания за групово финансиране, беше пусната онлайн версия на Twilight Struggle за компютри и Mac („достъпна в Steam“). След като изиграх няколкостотин онлайн игри, реших, че искам да се опитам да създам система за измерване на късмета, за да оценя собствените си резултати. В процеса започнах да събирам резултатите за хвърляния на зарчета за „преврати“ и „военни карти“. И тук нещата стават интересни: пробите ми с хвърляне на зара имаха изненадващи разпределения.

След 279 хвърляния средното ми хвърляне беше 3,254. След 303 хвърляния средното хвърляне на опонента ми беше 3,855. Исках да разбера колко необичайно е това разпределение, затова проведох няколко хи-квадрат теста в Python.

Разбиране на хи-квадрат тестове

Преди да разгледаме тези тестове обаче, ще обясня хи-квадрат по-подробно.

Статистическият тест хи-квадрат се използва, за да се определи дали има значителна разлика между очаквано разпределение и действително разпределение. Обикновено се използва с категорични данни като образователни постижения, цветове или пол.

Хвърлянията на зарове са чудесен пример за данни, подходящи за тестване на хи-квадрат. Ако хвърлим стандартен 6-странен зар хиляда пъти, знаем, че всяко число трябва да се появи приблизително 1/6 от времето (т.е. 16,66667%). Тестът хи-квадрат може да помогне да се определи дали зарът е „справедлив“ или генераторите на зарчета (като тези, използвани в софтуера) генерират „случайни“ резултати.

Въпреки това, хвърлянията на зара са пример за променлива, при която „очакваното разпределение“ е известно. Това не винаги е така. Понякога нашето „очаквано разпределение“ се оценява чрез данни.

Нека се престорим за секунда, че не знаем очакваната честота на хвърляния на зара. Ще трябва да оценим очакваната честота чрез извадки от данни. Нека направим няколко проби, за да се опитаме да установим честотата на всяко хвърляне. Реших да направя 4 проби от хвърляния на зара ръчно (т.е. с истински зарове), първите 3 проби бяха по 35 хвърляния всяка, а последната проба беше 45 хвърляния. Това са по-малки проби, отколкото предпочитаме, но исках да ни дам някои реални данни, с които да работим. Ето моето разпределение на рула, като четирите проби са обозначени с буквите „a“, „b“, „c“ и „d“.

Като се има предвид това, което знаем за вероятността, при 150 хвърляния трябва да очакваме всяко число да се появи приблизително 25 пъти (т.е. 1/6 от 150). Можем да видим, че това се случи за 1, 5 и 6, но 4 излезе доста повече от очакваното, а 2 и 3 бяха малко по-слабо представени. Това вероятно се дължи на нашия сравнително малък размер на извадката (вижте „„закона на големите числа““), но ние ще работим с него.

Нека изпълним „хи-квадрат тест за независимост“ за променливи в таблица за непредвидени обстоятелства върху този набор от данни. Първо ще въведа данните.

import numpy as np
a1 = [6, 4, 5, 10]
a2 = [8, 5, 3, 3]
a3 = [5, 4, 8, 4]
a4 = [4, 11, 7, 13]
a5 = [5, 8, 7, 6]
a6 = [7, 3, 5, 9]
dice = np.array([a1, a2, a3, a4, a5, a6])

След това проведох теста с помощта на библиотеката SciPy Stats

from scipy import stats

stats.chi2_contingency(dice)

За съжаление, въпреки че е много полезен инструмент, SciPy не предоставя резултатите по най-красивия начин.

Ще обясня какво означава всичко това. Първата стойност (16,49) е хи-квадрат статистика. Прескочете надолу до третото число в изхода; това са „степени на свобода“. Това може да се изчисли, като се вземе броят на редовете минус едно и се умножи този резултат по броя на колоните минус едно.

В този случай:

Редове = 6 [хвърляния на матрицата 1–6]

Колони = 4 [проби]

Така че вземаме (6–1) и умножаваме по (4–1), за да получим 15 степени на свобода.

С помощта на хи-квадрат и степените на свобода можем да намерим p-стойността. P-стойността е това, което използваме, за да определим значимостта (или независимостта в този случай). В зависимост от теста, обикновено търсим праг от 0,05 или 0,01. Нашият тест е важен (т.е. отхвърляме нулевата хипотеза), ако получим p-стойност под нашия праг.

За нашите цели ще използваме 0,01 като праг. В този конкретен пример p-стойността (второто число в нашия изход: 0,3502) е далеч от 0,01 и по този начин не сме покрили прага за статистическа значимост.

Сега, след като обясних какво означава всичко, можем да създадем по-лесен за четене изходен код в SciPy.

chi2_stat, p_val, dof, ex = stats.chi2_contingency(dice2)
print("===Chi2 Stat===")
print(chi2_stat)
print("\n")
print("===Degrees of Freedom===")
print(dof)
print("\n")
print("===P-Value===")
print(p_val)
print("\n")
print("===Contingency Table===")
print(ex)

Това ще доведе до много по-последователен резултат:

И накрая, масивът в края на изхода е таблицата за непредвидени обстоятелства с очаквани стойности, базирани на всички наши проби. Обърнете внимание, че в този случай нашата таблица за непредвидени обстоятелства дава стойности, които в някои случаи са доста по-различни от това, което знаем, че трябва да очакваме с хвърляния на зара. Това е така, защото използваме твърде малка извадка за точно измерване на популацията.

Изготвяне на голяма извадка за получаване на очакваното разпределение на популацията

Можем да направим много по-голяма извадка, за да видим как тази методология може да работи по-добре. Тъй като не желая да хвърлям на ръка зар хиляди пъти, ще използваме Python, за да направим това. Имаме нужда от np.random.randint и np.unique. Направих 5 проби от по 1000 хвърляния на зарчета.

r1 = np.random.randint(1,7,1000)
r2 = np.random.randint(1,7,1000)
r3 = np.random.randint(1,7,1000)
r4 = np.random.randint(1,7,1000)
r5 = np.random.randint(1,7,1000)

След това запази резултатите чрез np.unique.

unique, counts1 = np.unique(r1, return_counts=True)
unique, counts2 = np.unique(r2, return_counts=True)
unique, counts3 = np.unique(r3, return_counts=True)
unique, counts4 = np.unique(r4, return_counts=True)
unique, counts5 = np.unique(r5, return_counts=True)

Сега комбинираме нашите масиви, за да изпълним stats.chi2_contingency:

dice = np.array([counts1, counts2, counts3, counts4, counts5])

И нека да проведем теста.

chi2_stat, p_val, dof, ex = stats.chi2_contingency(dice)

Ето ги резултатите.

Забележете, че нашата таблица за непредвидени обстоятелства сега произвежда по-равномерно очаквано разпределение. Все още е леко отклонение (трябва да очакваме всяко число да се появи около 166,7 пъти в извадка от 1000 хвърляния на зар), но се доближаваме много до това разпределение.

Реших да пусна теста още веднъж, този път с 5 проби от 10 000 хвърляния на матрицата.

Можем да видим, че нашето разпределение се доближава още повече до известното разпределение на населението (16,667% шанс за всяко число или 1666,7 от 10 000 ролки в извадката). Интересното в това е, че тъй като знаем реалното очаквано разпределение, можем да видим как извадките ни позволяват да оценим разпределението на населението.

Тест хи-квадрат със зарове на здрач

Сега нека преминем към нашите онлайн данни за зарове Twilight Struggle.

За нашия действителен тест не се нуждаем от таблицата за непредвидени обстоятелства. Знаем очакваното разпределение. За 6-странен зар се очаква всяко число да излезе приблизително 1/6 от времето. Тъй като знаем очакваното разпределение, можем да използваме scipy.stats.chisquare вместо chi2_contingency.

За моите данни за зарове Twilight Struggle имам две проби: хвърляния на зар за себе си и хвърляния на зар за опонентите ми. Нашата нулева хипотеза е, че хвърлянията на зара са произволно разпределени (и следователно, равномерно разпределени).

За моите данни хвърлих 279 пъти. Делим на 6, за да намерим очакваното разпределение (46,5 за всяко число). Когато стартирате scipy.stats.chisquare, внимавайте да получите правилния ред на аргументите; в противен случай ще получите неточни резултати. Първият аргумент (f_obs) е за „действителните резултати“, докато вторият аргумент (f_exp) е за „очаквани резултати“.

my_rolls_expected = [46.5, 46.5, 46.5, 46.5, 46.5, 46.5]
my_rolls_actual =  [59, 63, 37, 38, 32, 50]
stats.chisquare(my_rolls_actual, my_rolls_expected)

Изпълнявайки този тест, получаваме p-стойност от 0,0037.

Това е под 0,01 и е статистически значимо. Това означава, че има само около 0,4% шанс да видим този резултат, ако заровете са наистина произволни.

След това нека да разгледаме хвърлянията на зара на моя опонент. Опонентите ми хвърлиха 303 пъти. Още веднъж разделяме на 6, за да намерим очакваното разпределение на 50,5. Сравняваме с действителното разпределение.

opp_rolls_expected = [50.5,50.5,50.5,50.5,50.5,50.5]
opp_rolls_actual =  [39,39,46,54,53,72]
stats.chisquare(opp_rolls_actual, opp_rolls_expected)

Откриваме подобен резултат.

Нашата p-стойност е 0,0097, което също е под 0,01, което показва, че има малко по-малко от 1% шанс да наблюдаваме това разпределение, ако хвърлянията на зарове са наистина рандомизирани.

Въпреки че случайно забелязах странни модели в хвърлянията на матрицата (които преди това бях отхвърлил като „пристрастност към наблюдението“), всъщност съм малко изненадан от тези резултати. Както за моите хвърлени зарчета, така и за хвърлянията на опонента в нашите 2 произволни извадки можем да отхвърлим нулевата хипотеза, че заровете са наистина произволни.

Това е интересно и реших, че ще продължа да събирам данни, за да видя дали мога да възпроизведа резултатите в бъдеще. За съжаление, вероятно ще ми отнеме още няколко месеца, за да изградя няколко по-смислени извадки от данни.

Изводи

Хи-квадрат е чудесен инструмент за сравняване на резултати, включващи категорични данни. Можем да видим как една извадка се отклонява от очакваното разпределение. Библиотеката SciPy на Python предоставя страхотни инструменти за провеждане на хи-квадрат тестове.

Допълнителни ресурси

За да разберете по-добре хи-квадрат, препоръчвам „отличната поредица от видеоклипове“ на Khan Academy.