PunchEPL: João Pedro wins EA Sport Premier League August player awardDaily MaverickTHE GATHERING 2026: ‘Not too late to turn around SA’s municipalities with the right people in charge’CNN TürkESMA'dan piyasa uyarısı: Ani düzeltme riski artıyorThe Jerusalem PostRosh Hashanah: The time to look inward and reveal what is unseenInquirerDOJ respects court decision as Sara Duterte arraignment deferredוואלהבכיר בממשלת תימן: החות'ים השתלטו על האי האסטרטגי מיוןInquirer EntertainmentPH-Australian film ‘First Light’ chosen as Australia’s official entry to 2027 OscarsBollywood HungamaSagar Pictures Entertainment unveils first teaser poster of Shrimad Bhagavatam Part I: titled VishnurataScreen RantDungeon Crawler Carl: Crawler Chaos Officially Lands November 2026CBS NewsWoman who answered first 9/11 call meets flight attendant's family 25 years laterColliderDenzel Washington’s Forgotten ‘Se7en’ Replacement Officially Takes Over NetflixSCMP ChinaSingapore and mainland China top global student assessment as OECD scores hit record low
The Daily Newsstand · Free, Always
Friday, September 11, 2026

Контринтуиция в статистике на примере игры Мир Танков/WOT

Translate

Контринтуитивные события окружают нас повсюду. Взять, например, парадокс дней рождений: каждый раз, когда оказываешься в небольшой группе и узнаёшь, что у двух участников группы праздник в один и тот же день, то думаешь: какое удивительное совпадение, ведь кажется, что событие редкое. Но вот уже при детальном рассмотрении оказывается, что вероятность такого события достаточно высокая (например, если группа 23 человека, то вероятность составит 50%, для группы в 57 человек — 99%). Примеров контринтуитивных событий много и они удивительны тем, что их разоблачение позволяет нам менять своё мировосприятие. 

Вот и мы с друзьями затеяли спор об одном таком событии, связанным с игрой Мир танков. Суть спора: двое из нас считают один сервер «несчастливым», потому что при нашей средней статистике в 50%, именно на этом сервере происходят частые поражения. Третий друг считает, что не может быть такого, чтоб сервер был «несчастливым» и тем более, невозможно, чтобы средний процент побед всех игроков на нём был меньше 50%. 

Его аргументы: исход боя либо победа, либо поражение (ничью, которая является поражением для обеих команд — в расчёт не берём). Вот мы берём 30к игроков, они играют между собой (команды набираются по 15 игроков), допустим, 30 боёв, статистику каждого записываем и получаем средний процент побед игроков в 50% (кто‑то выиграл 10 из 30, кто‑то 20 из 30, тем самым, получаем среднее). И вроде всё так. Но в такой модели мы говорим: каждый обязан провести ровно 30 игр. В жизни так не бывает, существует очень много параметров, всех их не рассмотреть. Но я уверен, что даже один параметр уже будет смещать среднее вправо или влево от теоретических 50%. Самый главный параметр — количество игр. Все играют по‑разному. У кого‑то времени больше, у кого‑то меньше. И сначала я захотел провести эксперименты именно с этим параметром.

Логика эксперимента

Для начала опишу логику: что и как мы будем смотреть, проводить, измерять. Определимся с количеством игроков. Могу по опыту сказать, что в вечернее время на сервере играет около 30к игроков. То есть количество игроков онлайн 30 000. Это константа, одни игроки уходят, другие заходят, но онлайн в эксперименте всегда ровно 30 000. Сколько будет длиться эксперимент — количество игр/итераций равно 30. Исходя из опыта, 30 игр это примерно 4–5 часов реального времени, онлайн в 30к примерно столько и держится. Ожидание и подбор соперников из эксперимента убраны.

Как происходит эксперимент: играют все 30к одновременно, то есть сначала набираются матчи (в матче 30 игроков), получается 1000 матчей. В каждом матче игроки случайно формируют команды (15 в одной команде, оставшиеся 15 в другой). Проводится матч, игрокам, которые победили, записывается 1 победа — так ведётся статистика каждого игрока, сколько матчей он сыграл и сколько матчей он выиграл. После одного матча все игроки перемешиваются по матчам, потом по командам и опять проводится игра. Также, каждому игроку случайным образом присваивается параметр — сколько матчей он сыграет. Например, игрок с id 1 должен сыграть 10 матчей. После 10-го матча он «уходит» из игры (но его статистика сохраняется), на его место приходит игрок, который должен сыграть 5 матчей и так далее. Если осталась последняя игра (всего 30 игр), а пришёл игрок у которого параметр 10 игр — он сыграет всего 1 игру, и его статистика запишется. Параметр «количество игр одного игрока» будет описываться различными распределениями, о которых поговорим далее.

Нормальное распределение

Нормальное распределение — это непрерывное распределение вероятностей с пиком в центре и симметричными боковыми сторонами. Формула выглядит следующим образом:

f(x) = \frac{1}{\sigma \sqrt{2\pi}} \exp\left(-\frac{(x-\mu)^2}{2\sigma^2}\right)

Это распределение берут, когда есть какое‑то среднее, и мы считаем что 99,99% всех значений лежит в 3-х среднеквадратичных отклонениях вправо и влево. Вот и я в этом эксперименте решил использовать это распределение как параметр количества игр у игрока. 

Предполагаем, что в среднем игрок проводит 10 игр, значит, что дисперсия будет 3,5427, а среднее квадратичное отклонение 1,88. Ниже я оставлю код, чтобы вы могли воспроизвести этот эксперимент у себя. Следующие эксперименты будут отличаться только распределениями и их параметрами.

import random
import pandas as pd
import math
from scipy.stats import norm

# ========== КОНСТАНТЫ ==========
PRE_TOTAL_PLAYERS = 30000      # начальное количество активных игроков
TEAM_SIZE = 15
MATCHES_PER_ROUND = PRE_TOTAL_PLAYERS // (TEAM_SIZE * 2)  # 1000
TOTAL_ROUNDS = 30              # максимальное количество раундов (т.к. максимум 30)

# Параметры нормального распределения для max_games
MEAN_MAX_GAMES = 10.0
STD_MAX_GAMES = 1.88
MIN_MAX_GAMES = 3
MAX_MAX_GAMES = 30

# ========== ФУНКЦИЯ ГЕНЕРАЦИИ ЛИМИТА ==========
def generate_max_games():
    """Генерирует целое число боёв для нового игрока из усечённого нормального распределения."""
    # Генерируем значение из нормального распределения
    raw = norm.rvs(loc=MEAN_MAX_GAMES, scale=STD_MAX_GAMES)
    # Обрезаем до допустимого диапазона
    clamped = max(MIN_MAX_GAMES, min(MAX_MAX_GAMES, raw))
    # Округляем вверх до целого
    return math.ceil(clamped)

# ========== ИНИЦИАЛИЗАЦИЯ ДАННЫХ ==========
# Создаём начальных игроков с нормально распределёнными лимитами
active_players = []
max_games_dict = {}
stats = {}

print("=== Инициализация ===")
print(f"Генерация {PRE_TOTAL_PLAYERS} начальных игроков...")
for i in range(PRE_TOTAL_PLAYERS):
    player_id = i
    max_g = generate_max_games()
    active_players.append(player_id)
    max_games_dict[player_id] = max_g
    stats[player_id] = {'wins': 0, 'games': 0}

next_id = PRE_TOTAL_PLAYERS
print(f"Сгенерировано {len(active_players)} игроков. Следующий ID: {next_id}")

# Посчитаем распределение лимитов для справки
from collections import Counter
init_counts = Counter(max_games_dict.values())
print("Распределение max_games среди начальных игроков (первые 10 значений):")
for g in sorted(init_counts.keys())[:10]:
    print(f"  {g}: {init_counts[g]} игроков")
print("...")

# ========== ФУНКЦИЯ ПРОВЕДЕНИЯ ОДНОГО РАУНДА ==========
def run_round(active_players, stats):
    """Проводит один раунд (MATCHES_PER_ROUND матчей) среди активных игроков."""
    shuffled = active_players.copy()
    random.shuffle(shuffled)

    for match_idx in range(MATCHES_PER_ROUND):
        start = match_idx * (TEAM_SIZE * 2)
        match_players = shuffled[start:start + (TEAM_SIZE * 2)]

        team1 = match_players[:TEAM_SIZE]
        team2 = match_players[TEAM_SIZE:]

        winner = random.choice([1, 2])
        winning_team = team1 if winner == 1 else team2
        all_players = team1 + team2

        for pid in winning_team:
            stats[pid]['wins'] += 1
        for pid in all_players:
            stats[pid]['games'] += 1

    return stats

# ========== ОСНОВНОЙ ЦИКЛ СИМУЛЯЦИИ ==========
if __name__ == '__main__':
    print("\nНачинаем симуляцию на {} раундов...".format(TOTAL_ROUNDS))
    for round_num in range(1, TOTAL_ROUNDS + 1):
        stats = run_round(active_players, stats)
        print(f"Раунд {round_num} завершён.")

        # Если это не последний раунд, выполняем замену завершивших игроков
        if round_num < TOTAL_ROUNDS:
            # Находим игроков, достигших своего max_games
            players_to_remove = [
                pid for pid in active_players
                if stats[pid]['games'] == max_games_dict[pid]
            ]

            # Удаляем их из активного списка
            for pid in players_to_remove:
                active_players.remove(pid)

            # Создаём новых игроков с новыми лимитами (по одному на каждого удалённого)
            for _ in players_to_remove:
                new_id = next_id
                next_id += 1
                new_max = generate_max_games()
                max_games_dict[new_id] = new_max
                stats[new_id] = {'wins': 0, 'games': 0}
                active_players.append(new_id)

            print(f"  Заменено {len(players_to_remove)} игроков. Активных: {len(active_players)}")

    print("\n=== Симуляция завершена ===")
    print(f"Всего уникальных игроков создано: {next_id}")

    # ========== ПРОВЕРКИ ==========
    total_games = sum(record['games'] for record in stats.values())
    total_wins = sum(record['wins'] for record in stats.values())

    print(f"Общее количество сыгранных матчей (игр) на всех игроков: {total_games}")
    print(f"Общее количество побед: {total_wins}")

    # Проверка, что все игроки не превысили свой лимит
    violations = 0
    for pid, record in stats.items():
        if record['games'] > max_games_dict[pid]:
            violations += 1
    print(f"Игроков, превысивших лимит: {violations}")

    # Количество игроков, которые завершили свою карьеру (games == max_games)
    finished = sum(1 for pid in stats if stats[pid]['games'] == max_games_dict[pid] and stats[pid]['games'] > 0)
    print(f"Игроков, завершивших карьеру (сыграли ровно свой лимит): {finished}")

    # ========== СОХРАНЕНИЕ В EXCEL ==========
    try:
        data = []
        for pid, record in stats.items():
            games = record['games']
            wins = record['wins']
            win_rate = (wins / games * 100) if games > 0 else 0.0
            data.append({
                'player_id': pid,
                'max_games': max_games_dict[pid],
                'games': games,
                'wins': wins,
                'win_rate': win_rate
            })

        df = pd.DataFrame(data)
        df.sort_values('player_id', inplace=True)

        filename = 'statistics_normal_distribution.xlsx'
        df.to_excel(filename, index=False)
        print(f"\nСтатистика сохранена в файл {filename}")
    except Exception as e:
        print(f"Ошибка при сохранении: {e}")

    # ========== ДОПОЛНИТЕЛЬНАЯ СВОДКА ПО ВСЕМ ИГРОКАМ ==========
    avg_win_rate_all = sum(stats[pid]['wins'] / max(1, stats[pid]['games']) * 100 for pid in stats) / len(stats)
    print(f"\nСредний процент побед по всем игрокам (включая активных): {avg_win_rate_all:.2f}%")

    # Сводка только по завершившим (для сравнения)
    if finished > 0:
        finished_players = [pid for pid in stats if stats[pid]['games'] == max_games_dict[pid] and stats[pid]['games'] > 0]
        avg_win_rate_finished = sum(stats[pid]['wins'] / stats[pid]['games'] * 100 for pid in finished_players) / len(finished_players)
        print(f"Средний процент побед только среди завершивших карьеру: {avg_win_rate_finished:.2f}%")

    # Пример для первого игрока
    if 0 in stats:
        print(f"\nПример: игрок 0 (max_games={max_games_dict[0]}) "
              f"сыграл {stats[0]['games']} игр, побед {stats[0]['wins']} "
              f"(процент {stats[0]['wins']/max(1, stats[0]['games'])*100:.2f}%)")

Сразу хочется сказать — очевидно, что один результат хоть и покажет то, что мы ожидаем, но для чистоты эксперимента я провёл 100 симуляций, чтобы видеть более общую картинку. Итоги эксперимента графически:

Результаты среднего процента побед игроков в 100 симуляциях

Результаты среднего процента побед игроков в 100 симуляциях

Здесь мы видим, что хоть и средний процент побед игроков в каждой симуляции стремился к 50%, но почти ни в одной не был 50%. Разброс хоть и небольшой, в пределах 0,07 в каждую сторону, но даже так мы получаем ответ — если учитывать такой параметр, что каждый игрок играет определённое количество игр, то средний процент игроков будет смещаться либо вправо, либо влево.

Посмотрим каким образом выбирался параметр игроков:

Частотность по лимитам боёв

Частотность по лимитам боёв

Теперь посмотрим распределение игроков по проценту побед:

В каждой симуляции генерируется по итогу 100-110к игроков

В каждой симуляции генерируется по итогу 100–110к игроков

Мы видим, что график первой симуляции несимметричен. Асимметрию создают игроки, которые проводят случайное количество матчей. Например, очевидно, что игроки у которых 0% и 100% побед играли совсем мало матчей, может, выиграли 5 из 5 или проиграли 3 из 3. 

Ради интереса я решил проверить — является ли распределение процента побед игроков нормальным. Для этого я использовал график qq‑plot.

Видно, что в середине графика идёт совпадение с нормальными значениями (красная линия — нормальное распределение, синий график — распределение процента побед по игрокам), но хвосты всё портят

Видно, что в середине графика идёт совпадение с нормальными значениями (красная линия — нормальное распределение, синий график — распределение процента побед по игрокам), но хвосты всё портят

Ну и на всякий случай, решил использовать тест Колмогорова‑Смирнова для определения нормальности в каждой симуляции.

Чтобы не растягивать статью скажу: ни в одном эксперименте, распределение процента побед игроков не было нормальным

Чтобы не растягивать статью скажу: ни в одном эксперименте, распределение процента побед игроков не было нормальным

Распределение Пуассона

Распределение Пуассона применяется для подсчёта событий, которые происходят случайно, независимо и с постоянной средней интенсивностью, используется для определения потока посетителей в магазине, звонков в колл‑центры в час и так далее

P(X = k) = \frac{\lambda^k e^{-\lambda}}{k!}, \quad k = 0,1,2,\dots, \quad \lambda > 0

Оно будет несимметричным, поэтому я ради эксперимента его тоже взял. Вот так будет выглядеть распределение параметров, при лямбда 10 игр:

Распределение лимитов стало более широким и асимметричным

Распределение лимитов стало более широким и асимметричным

Эксперимент при 100 симуляциях дал следующие результаты:

Видим всё такое же отклонение среднего процента побед всех игроков

Видим всё такое же отклонение среднего процента побед всех игроков

Распределение игроков по проценту побед:

Тут мы видим более жирные хвосты тех у кого 0% и 100% побед, как раз за счёт  асимметрии распределения Пуассона, то есть распределение стало давать больше вероятности, что зайдёт игрок, который играет мало матчей

Тут мы видим более жирные хвосты тех у кого 0% и 100% побед, как раз за счёт асимметрии распределения Пуассона, то есть распределение стало давать больше вероятности, что зайдёт игрок, который играет мало матчей

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

Распределение Парето

Распределение Парето — это степенное математическое распределение непрерывной случайной величины, которое описывает дисбаланс и неравномерность в экономике, природе и обществе.

F(x) = P(X \le x) =  \begin{cases} 1 - \left(\dfrac{x_m}{x}\right)^\alpha, & x \ge x_m, \\[6pt] 0, & x < x_m, \end{cases} \quad \text{где } x_m > 0,\; \alpha > 0.

Параметр распределения по играм получился в таком виде:

То есть большинство игроков сыграло только 4 игры, уже гораздо меньше 5 и так далее

То есть большинство игроков сыграло только 4 игры, уже гораздо меньше 5 и так далее

С одной стороны такое распределение даст нам хорошую симметрию, за счёт того, что сыграть 4 победы из 4 боёв или проиграть все из 4-х, шанс не такой уж и большой, что мы и увидим на следующих графиках:

Распределение 100 симуляций отличается от предыдущих минимальным разбросом

Распределение 100 симуляций отличается от предыдущих минимальным разбросом

Распределение игроков выглядит гораздо интереснее, чем раньше:

Распределение с самыми жирными хвостами

Распределение с самыми жирными хвостами

Тут примерно 12к игроков не выиграли ни одной игры и 12к игроков выиграли все игры. Как раз из‑за того, что игр у большинства игроков было мало.

Отрицательное биномиальное распределение

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

P(X = k) = \binom{k+r-1}{k} p^r (1-p)^k, \quad k = 0,1,2,\dots

Распределение лимита боёв будет выглядеть следующим образом:

Большинство проводит 3 игры, а остальное распределение равномерно угасает, исключение составляют лишь «старички», которые просто хотят поиграть подольше

Большинство проводит 3 игры, а остальное распределение равномерно угасает, исключение составляют лишь «старички», которые просто хотят поиграть подольше

В таком распределении результат 100 симуляций выглядит так:

Разброс примерно 0,1 в каждую сторону

Разброс примерно 0,1 в каждую сторону

Распределение по проценту побед у игроков выглядит так:

Тут хвост получился гораздо меньше, чем у Парето, всего по 6к игроков либо все игры выиграли, либо все проиграли. Но всё ещё больше, чем у распределений Пуассона (2,5к)  и нормального (2к)

Тут хвост получился гораздо меньше, чем у Парето, всего по 6к игроков либо все игры выиграли, либо все проиграли. Но всё ещё больше, чем у распределений Пуассона (2,5к) и нормального (2к)

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

Второй и главный вывод этого эксперимента: даже если в игре всего 2 исхода, это не означает, что средний процент всех игроков будет 50%, он будет смещаться влево или вправо, в зависимости от случайности. 

Последний эксперимент

Мы получили эксперимент, который не может похвастаться большим разбросом от теоретического значения. Но мы добавили лишь один математический параметр — количество игр каждого игрока. А что если добавить ещё один параметр? Например, психологический. Далеко не каждый игрок может мириться с луз стриками. Ладно 2–3 поражения подряд. А что если мы опишем функцию «выживания» игрока. Возьмём за параметр, что в среднем игрок уйдёт после 5 поражения подряд. Опишем это также: отрицательным биномиальным распределением. Тогда будет следующая логика: игрок зашёл сыграть 10 матчей, но если произойдёт 5 поражений подряд, то он выйдет из игры (или перейдёт на другой сервер).

Тогда получается, что с таким распределением лимита боёв:

Распределение как и в предыдущем эксперименте

Распределение как и в предыдущем эксперименте

И с новым параметром выживаемости:

Распределение начинается с 2-х поражений подряд

Распределение начинается с 2-х поражений подряд

Мы получаем такое распределение среднего процента побед игроков по 100 симуляциям:

Видим сильное смещение влево за счёт добавления нового параметра

Видим сильное смещение влево за счёт добавления нового параметра

И следующее распределение процента побед игроков в первой симуляции:

Почему график сместился так сильно влево? Потому что много игроков не захотело мириться с тем, что у них несколько поражений подряд. Соответственно они ничего не выиграли и ушли, оставив за собой плохую статистику

Почему график сместился так сильно влево? Потому что много игроков не захотело мириться с тем, что у них несколько поражений подряд. Соответственно они ничего не выиграли и ушли, оставив за собой плохую статистику

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

При добавлении параметра «выживаемости» игрока, мы видим, что средний процент побед игроков сместился влево. Эксперимент можно назвать радикальным, потому что в распределении выживаемости много игроков, которые уйдут после 2-3-х поражений подряд. Но при этом, суть остаётся — при добавлении параметров, которые могут быть разными: психологическими, ситуативными, физическими, то даже в банальной игре с двумя возможными событиями — победа/поражение, средняя статистика игроков будет смещаться влево.

Чтобы не запутаться с дальнейшими данными реальных данных, повторю — изначальный спор был о том, может ли статистика игроков целого сервера в данных момент игрового времени быть меньше теоретических 50%? Ответ — да, может. Первое — за счёт того, что игроки играют разное количество матчей. Второе — за счёт психологического параметра выживаемости. И третье — малое количество игр. Так как мы рассматривали 30 игр, а это 4–5 часов реального времени.

Реальные данные

Меня всегда интересовало, как вообще распределён средний процент игроков в игре. Я нашёл сайт сессионной аналитики для игр «Мир танков» и «World of Tanks» и запросил у разработчика сайта данные. Он любезно их предоставил, чтобы можно было посмотреть средний процент игроков. Данные по RU региону, режим игры — «Случайный бой».

Тут мы видим жирные хвосты за счёт новичков, которые больше в игру не играли

Тут мы видим жирные хвосты за счёт новичков, которые больше в игру не играли

Теперь посмотрим статистику тех, кто отыграл больше 1000 игр:

На графике хорошо видна асимметрия, что меньше 40% игроков почти нет, но больше 60% есть и не мало

На графике хорошо видна асимметрия, что меньше 40% игроков почти нет, но больше 60% есть и не мало

Ну и посмотрим ветеранов игры, которые провели более 10 000 игр

Статистика говорит о том, что среднее ветеранов составляет аж 51,46%

Статистика говорит о том, что среднее ветеранов составляет аж 51,46%

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

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.