Чувствительность сравнения
Эта страница фиксирует два теста устойчивости методом Монте-Карло, стоящие за картой сравнения: передискретизацию весов осей по всему их пространству при замороженных подоценках и возмущение каждой подоценки не более чем на пункт при опубликованных весах. Она держит конструкции и зерна, интервалы рангов для всех двадцати двух продуктов, статистику пересечений в прямых сравнениях, одно отозванное утверждение и скрипт, который воспроизводит запись.
Первая версия страницы методологии проверяла ровно один альтернативный весовой набор и сообщала об одном пересечении. Рецензирование сделало то, что должны были сделать мы: Монте-Карло по всему весовому пространству и по неопределённости подоценок. Мы переняли конструкцию и прогнали её заново собственным кодом, а не цитируя числа рецензента. Эта страница публикует наши; полная запись прогонов и проверка согласия с цифрами рецензента — в review/index/SCORES.md, а каждая подоценка, на которую опираются тесты, — на странице оценок.
Два теста
Каждый тест прогоняет N = 200 000 выборок на ось:
- Веса выбираются, оценки заморожены. Неотрицательные веса разыгрываются равномерно по весовому симплексу каждой оси (нормализованные розыгрыши Exp(1)); каждая опубликованная подоценка удерживается фиксированной. Это спрашивает: какая часть упорядочения — наше суждение о весах?
- Веса заморожены, оценки возмущены. Опубликованные веса сохраняются; к каждой подоценке каждого продукта добавляется независимый равномерный шум ±1 с отсечением в 0–10. Это спрашивает: сколько зависит от того, что целочисленные суждения ровно верны?
Ранг здесь — это 1 + число продуктов со строго более высокой оценкой. Основной прогон: зерно 20260807. Независимая переимплементация (другой код и генератор, зерно 271828). Пересчитано 2026-08-07 по полю из 22 продуктов, когда была оценена Obscura: ранг относителен полю, поэтому интервалы ниже были прогнаны заново со строкой Obscura внутри, а не оставлены протухшими, и рабочая запись в review/index/SCORES.md сохраняет прогоны по 21 продукту как запись воспроизведения рецензента. Пересчитано снова в тот же день, когда URnetwork был переоценён по постановлению о включении по умолчанию (реестр методологии): таблицы ниже — это запись после переоценки, прогнанная заново под обеими реализациями и обоими зернами, а допереоценочные прогоны остаются в таблице прогонов рабочей записи. На поле из 22 продуктов две реализации согласуются по каждому интервалу и медиане, кроме пяти ячеек весового теста у почти равных соседних пар (расхождение 0.05 в точечной оценке), отмеченных в рабочей записи; те же пять ячеек различаются до и после переоценки. Каждый интервал шума оценок — тот род, который публикует обзорная страница, — идентичен в обоих прогонах, как и строки URnetwork и Obscura целиком.
Результаты URnetwork
Вход Obscura сдвинул статистику ранга по X: медиана по весам 3→4, интервал по шуму 2–3 → 2–4. Переоценка по умолчанию сдвинула их снова: медиана по весам 4→3, интервал по весам 2–5 → 1–4. Ни одна ранговая статистика теста шума и ни одна статистика по Y не сдвинулись ни в тот, ни в другой раз.
| Статистика | Тест 1, веса выбираются | Тест 2, шум оценок ±1 |
|---|---|---|
| Ранг X, медиана (5-й–95-й) | 3 (1–4) | 3 (2–4) |
| Оценка X, 5-й–95-й | (веса варьируются) | 6.793–7.813 |
| Впереди Mullvad по X | 74.0% (ровно 71/96) | 99.9% |
| Впереди Obscura по X | 100% (вырожденно; см. ниже) | 93.9% |
| Впереди NymVPN по X | 67.0% (ровно 343/512) | 30.6% |
| Ранг Y, медиана (5-й–95-й) | 3 (1–15) | 7 (4–11) |
| Оценка Y, 5-й–95-й | (веса варьируются) | 5.747–6.653 |
| Y ниже 5.00 | никогда, по построению (см. отозванное утверждение) | 0 из 200 000 (около 4.4σ при этой модели шума: редко, но не невозможно) |
Две оси не несут равной уверенности. Ранг X у URnetwork остаётся внутри 1–4 при каждом выбранном весовом наборе: у верха оси X порядок решает то, какое суждение вы делаете, но сам индекс сравнительно устойчив к весам. Его ранг Y бегает по 1–15 в том же весовом пространстве, и даже при опубликованных весах медиана при шуме ±1 — 7-е место, а не делённое 5-е, которое подсказывает его точечная оценка. Индекс Y гораздо чувствительнее к весам, чем X, и каждое размещение по Y на карте следует читать с соответственно меньшей уверенностью.
Прямые сравнения
Mullvad. Два числа по Mullvad остаются самым информативным предложением, доступным об этом графике, и переоценка по умолчанию изменила то, что они говорят. До неё отрыв URnetwork от Mullvad по X составлял 55.55% при неопределённости весов — подбрасывание монеты, которое почти целиком зависело от ранжирования структуры (отделение плюс сквозное шифрование, 0.60 суммарно) выше проверки (0.30). После переоценки разница подоценок составляет (+5, +3, −3, −1): URnetwork ведёт по обеим структурным частям, Mullvad ведёт по проверке и, узко, по постквантовости, а та же выборка весов даёт URnetwork отрыв в 74.0% случаев (точный объём симплекса — 71/96 ≈ 73.96%). При шуме оценок ±1 при опубликованных весах это 99.9%. Отрыв больше не подбрасывание монеты, но остаточная чувствительность к весам выживает в очерченной форме: читатель, который взвешивает проверку примерно в половину оси, по-прежнему получает Mullvad впереди, и это остаётся разумным прочтением, а не ошибкой. Проработанный пример — в разделе о перевзвешивании ниже.
Obscura. Сравнение перевернулось вместе с переоценкой. Теперь двое идут вничью по структуре (E+S 16–16, оба звена по содержимому включены по умолчанию, у Obscura — по построению) и вничью по V, поэтому весь разрыв в 0.70 — это постквантовая часть, которой Obscura не имеет. Чистая разница по одной части, (0, 0, 0, +7), делает весовой тест вырожденным: любой весовой набор с ненулевым постквантовым весом сохраняет порядок, поэтому «100% выбранных весовых наборов» описывает форму различия, а не устойчивость. Информативный тест — шум оценок, при котором URnetwork ведёт в 93.9% случаев: за пределами досягаемости одного пункта, поскольку крупнейший размах в один пункт по одной части — 0.35 против разрыва в 0.70. Допереоценочная статистика была 69.5%, что не выходило за пределы этой досягаемости; при допереоценочных оценках разница составляла (−1, 0, 0, +5) с точной долей симплекса 5/6 = 83.33% (выборочно 83.4%). По-настоящему сблизила бы эту пару поставка постквантовости у Obscura.
NymVPN. Переоценка купила новое соседство в другом направлении: NymVPN, на 7.50, теперь сидит на 0.20 выше 7.30 у URnetwork — ближе, чем Obscura под ним. При опубликованных весах ведёт NymVPN. По выбранным весовым наборам URnetwork ведёт в 67.0%: разница составляет (−1, −1, −1, +7), поэтому порядок целиком зависит от того, превышает ли постквантовый вес одну восьмую, а точная доля — (7/8)³ = 343/512. При шуме оценок NymVPN сохраняет отрыв в 69.4% розыгрышей (URnetwork впереди в 30.6%). Эта пара напечатана как соседняя и неустоявшаяся — ровно так, как Obscura и URnetwork были до переоценки.
Интервалы рангов по продуктам
5-й–95-й процентиль с медианой в скобках, поле из 22 продуктов после переоценки, прогон переимплементации, зерно 271828. Строки URnetwork и Obscura идентичны при основном прогоне. Переоценка изменила шесть ячеек, все в колонке весов по X: URnetwork 2–5 (4) → 1–4 (3), Tor 1–4 → 1–5, Obscura 3–10 → 4–10, медиана Mullvad 3 → 4, IVPN 2–8 → 3–8, ExpressVPN 2–12 → 3–12.
| Продукт | Ранг X, веса | Ранг X, шум оценок | Ранг Y, веса | Ранг Y, шум оценок |
|---|---|---|---|---|
| Tor | 1–5 (1) | 1–1 (1) | 2–22 (19) | 21–22 (22) |
| NymVPN | 2–9 (3) | 2–3 (2) | 18–21 (20) | 20–21 (20) |
| URnetwork | 1–4 (3) | 2–4 (3) | 1–15 (3) | 4–11 (7) |
| Obscura | 4–10 (6) | 3–5 (4) | 9–19 (15) | 12–19 (16) |
| Mullvad | 1–7 (4) | 4–6 (5) | 4–12 (7) | 4–11 (7) |
| IVPN | 3–8 (5) | 5–7 (6) | 6–16 (11) | 6–13 (10) |
| Apple Private Relay | 5–15 (9) | 6–8 (7) | 13–22 (20) | 13–19 (17) |
| Windscribe | 7–14 (11) | 7–10 (8) | 1–5 (2) | 1–5 (3) |
| PIA | 7–15 (12) | 8–13 (10) | 9–15 (11) | 7–14 (11) |
| ExpressVPN | 3–12 (8) | 8–13 (10) | 4–10 (6) | 4–11 (7) |
| Proton VPN | 9–16 (14) | 9–15 (12) | 1–5 (3) | 1–6 (3) |
| NordVPN | 6–13 (10) | 9–15 (12) | 4–10 (6) | 4–11 (7) |
| Tailscale | 6–18 (13) | 10–16 (13) | 1–8 (3) | 1–3 (1) |
| Cloudflare WARP | 7–16 (11) | 11–16 (14) | 1–16 (5) | 1–6 (3) |
| Surfshark | 7–16 (12) | 11–17 (15) | 8–13 (9) | 5–12 (8) |
| Orchid | 9–17 (16) | 12–17 (15) | 19–22 (21) | 20–22 (21) |
| Mysterium | 16–18 (18) | 16–20 (18) | 7–20 (18) | 14–19 (17) |
| TunnelBear | 15–19 (17) | 16–20 (18) | 8–16 (13) | 9–16 (13) |
| Sentinel | 17–20 (20) | 17–20 (19) | 9–19 (16) | 15–19 (18) |
| IPVanish | 19–20 (19) | 17–21 (19) | 12–19 (16) | 9–16 (13) |
| Hotspot Shield | 21–21 (21) | 20–21 (21) | 8–18 (12) | 9–16 (13) |
| Hola | 22–22 (22) | 22–22 (22) | 6–20 (17) | 14–19 (17) |
Интервалы ничего не преувеличивают по случайности симметрии моделей. Модель шума ±1 намеренно проста и не является эмпирическим распределением ошибок; её урок в том, что уверенность в ранге требует неопределённости в назначенных оценках, а не только второго набора весов.
Воспроизвели ли мы рецензента?
Да, на поле из 21 продукта, которое измерял рецензент, до того как была оценена Obscura. Рецензент сообщил (зерно 20260807, 200 000 выборок, собственный код): 55.64% впереди Mullvad; оценка X 6.341–7.359 и оценка Y 5.749–6.652 при шуме; интервалы рангов 2–5, 1–15, 2–3 и 4–11 при медиане Y 7. Наши прогоны на том поле вернули идентичные интервалы рангов и медианы, границы оценок в пределах 0.005 и 55.55% на пересечении; запись по 21 продукту сохранена в review/index/SCORES.md, а попарная статистика по Mullvad переносится на перепрогон по 22 продуктам без изменений в пределах ошибки выборки. У пересечения был точный ответ, по которому можно проверить всех: при оценках, которые измерял рецензент, разница подоценок URnetwork−Mullvad составляла (+4, +3, −3, −3), а доля весового симплекса на стороне URnetwork вычисляется как 109/196 ≈ 55.61%. Каждая оценка Монте-Карло, наша и рецензента, сидит в пределах ошибки выборки (±0.22 пункта на 2σ при таком размере выборки) от точного значения. Внешний результат, который мы не смогли бы воспроизвести, был бы находкой; тот, который мы воспроизвести можем, — тоже. Цифры рецензента — это допереоценочная запись: переоценка URnetwork по умолчанию позже в тот же день изменила его строку по X, а текущее точное значение пересечения — 71/96, выведенное и выбранное в таблицах выше. Проверка 109/196 по-прежнему воспроизводится на допереоценочных оценках — а это то, что измерял рецензент.
Отозванное утверждение, зафиксировано
До 2026-08-07 страница методологии утверждала, что размещение URnetwork по Y устойчиво к весам: что никакое перевзвешивание частей качества не может опустить его Y ниже 5.00. Вердикт рецензента — «математически истинно, но доказательно пусто» — принят полностью. Каждая подоценка Y у URnetwork была назначена ≥ 5, поэтому всякое выпуклое взвешенное среднее из них ≥ 5 по построению: мы опубликовали свойство собственных входных данных так, будто это свидетельство устойчивости, и утверждение осталось бы «истинным» независимо от того, насколько неверны были эти входные данные. То же возражение отправляет в отставку эквивалентное утверждение по оси X ещё до того, как его кто-нибудь сделает. Подоценки X у URnetwork тоже все ≥ 5, поэтому «никакое перевзвешивание не опустит X ниже 5.00» столь же гарантировано и столь же пусто. Ни одно из этих утверждений больше не появляется в этом корпусе как свидетельство. Что можно сказать по записи — в таблицах выше: при перевзвешивании информативный выход — это ранг, а не пересечение линии, а информативный тест самих линий — это неопределённость оценок, при которой 5-й процентиль Y у URnetwork равен 5.747 — утверждение о простой модели шума, а не о мире. Что действительно могло бы опустить Y ниже линии — это свидетельства: внешнее измерение, оценивающее задержку или пропускную способность ниже середины шкалы. Таких не существует ни в ту, ни в другую сторону, и правило корпуса о невыдуманных числах — причина, по которой две части производительности сидят на опорном уровне структурного класса 5. Утверждение отозвано, а не удалено, потому что ценность этой записи в том, что она хранит собственные ошибки.
Размещения внутри шума
Одноточечная чувствительность — не Монте-Карло, но она отвечает на тот же вопрос на границах, поэтому фиксируется здесь вместе с остальным.
- Apple Private Relay сидит внутри шума сразу от обеих линий, +0.10 по X и −0.05 по Y, в одиночестве на пересечении осей, — поэтому его следует читать как находящийся на линиях, а не как жителя какого-либо квадранта, включая ячейку «Академическое / исследовательское», в которую попадают его строгие знаки. Его суждения режут в обе стороны: оценка E или S на пункт ниже ставит X в 4.85 или 4.75, обратно левее линии, а оценка S на 8 ставит его отчётливо правее, в 5.45. Именно эта чувствительность — причина, по которой карта его аннотирует, а не заявляет.
- Obscura сидит внутри шума от линии Y сверху (+0.10). Её принадлежность к правой стороне линии X уверенна при +1.60 и устойчива к шуму: 0 из 200 000 выборок с шумом ±1 помещают её левее линии, при 5-м процентиле X 6.12. Впрочем, она чувствительна к весам: 32.6% равномерно выбранных весовых наборов помещают её левее, что вызвано PQ 0 и V 6, когда веса проверки и постквантовости разыгрываются высокими. Сторона Y — настоящее подбрасывание монеты, и на неё ничего не заявляется: Y падает ниже 5.00 в 36% выборок с шумом ±1 и в 50.0% выбранных весовых наборов, а единственная помеченная как суждение часть T двигает её в обе стороны (T 5 даёт 4.85, T 7 даёт 5.35).
- Mysterium (−0.10), Hola (−0.15) и Sentinel (−0.20) сидят внутри шума ниже линии Y, и их принадлежность к «выше/ниже» — не уверенное суждение; сигнал — их дефициты по X.
- Сверху TunnelBear, IPVanish и Hotspot Shield преодолевают линию Y всего на 0.45–0.50 на оценках L и T класса дата-центров; читатель, который оценил бы постоянство их флота на пункт ниже, поставил бы их на неё или под неё.
- Линия X — чистая вода для всех остальных. Никто, кроме Apple, не подходит к ней ближе 0.40 (Windscribe слева; IVPN на 0.70 и Obscura на 1.60 справа), поэтому никакая одноточечная переоценка не переводит через раздел «проверяемое/на доверии» ни один другой продукт. Apple — единственный продукт, чья сторона этой линии законно зависит от одного суждения.
Перевзвешивания, которые меняют порядок
Фиксированные веса ставят структуру (S + E = 0.60) выше проверки (V = 0.30). Перевзвесьте в сторону проверки достаточно сильно, например E 0.15 / S 0.25 / V 0.50 / PQ 0.10, — и Mullvad (7.00) обходит URnetwork (6.90) по оси; поднимите вес проверки ещё выше — и IVPN в конце концов тоже его обойдёт. До переоценки по умолчанию хватало более мягкого примера (V на 0.45), а число по всему пространству было подбрасыванием монеты: по равномерно выбранным весовым наборам URnetwork тогда опережал Mullvad лишь в 55.55% случаев. После переоценки он опережает в 74.0%, и пример с V 0.45 больше не переворачивает порядок (URnetwork 7.00, Mullvad 6.70). Порядок у верха оси теперь держится и на структурных подоценках, и на весах, но читатель, который взвешивает аудиты и судебные свидетельства примерно в половину оси, по-прежнему получает Mullvad впереди и не ошибается. Страницы по продуктам уже говорят такому читателю выбрать Mullvad или IVPN, пока URnetwork не закроет свой пробел с аудитом протокола.
Тот же рычаг, потянутый в другую сторону, — проработанный пример с IVPN: структурно-доминантный E 0.35 / S 0.45 / V 0.10 / PQ 0.10 опускает IVPN до 4.90, чуть левее линии, при Mullvad ровно на 5.00 на ней. Принадлежность IVPN к верхнему правому квадранту зависит от того, чтобы вес проверки оставался существенным. Замороженные веса его пропускают, и зависимость всё равно зафиксирована здесь.
На оси качества именно Eg с весом 0.20 поднимает запас URnetwork и держит WARP, Private Relay и Tor внизу. Читатель, который считает идентичность выхода нишевой, сжимает разброс по Y, и ранг Y у URnetwork бегает с 1-го по 15-е место по выбранному весовому пространству. Более ранняя версия страницы методологии закрывала этот анализ утверждением, что никакое перевзвешивание не может опустить Y у URnetwork ниже 5.00; это утверждение отозвано, а раздел выше фиксирует почему.
Что сдвинуло бы эту запись
Три вещи сдвинули бы результаты на этой странице, а не спорили бы с ними. Поставка постквантовости у Obscura закрыла бы единственную вырожденную пару (прямые сравнения выше). Внешнее измерение задержки или пропускной способности заменило бы структурные опорные уровни, которые раздел об отозванном утверждении называет мягкой почвой оси качества. И любая переоценка по действующему правилу страницы методологии прогоняет оба теста заново по всему полю — так, как это сделали добавление Obscura и переоценка по умолчанию.
Скрипт воспроизведения
Перекрёстно проверяющая реализация, на поле из 22 продуктов со строкой URnetwork после переоценки. Она перегенерирует свою строку таблицы прогонов в review/index/SCORES.md, ранговую статистику URnetwork и таблицу по продуктам выше (Python 3, numpy ≥ 2). Она утверждает, что каждая опубликованная сумма X и Y точно пересчитывается из матриц подоценок до выборки, а порядок розыгрышей важен для точного воспроизведения.
import numpy as np
SEED, N = 271828, 200_000
# name, X parts [X1 e2e, X2 sep, X3 verif, X4 pq], Y parts [Y1 L, Y2 T, Y3 Eg, Y4 In, Y5 A]
# URnetwork X row is the post-rescore [8, 8, 6, 7]; pre-rescore was [7, 8, 6, 5]
PRODUCTS = [
("URnetwork", [8, 8, 6, 7], [5, 5, 8, 8, 7]),
("Tor", [10, 10, 10, 4], [2, 1, 1, 9, 9]),
("NymVPN", [9, 9, 7, 0], [5, 4, 2, 5, 4]),
("Obscura", [8, 8, 6, 0], [5, 6, 5, 5, 4]),
("Mullvad", [3, 5, 9, 8], [7, 8, 5, 5, 4]),
("IVPN", [3, 5, 8, 8], [7, 7, 5, 4, 4]),
("Apple Private Relay", [7, 7, 3, 0], [8, 7, 2, 1, 2]),
("Windscribe", [3, 5, 7, 0], [7, 7, 6, 7, 7]),
("PIA", [3, 3, 8, 0], [7, 7, 4, 5, 4]),
("ExpressVPN", [3, 3, 5, 9], [7, 8, 4, 6, 4]),
("Proton VPN", [3, 3, 7, 0], [7, 8, 4, 6, 8]),
("NordVPN", [3, 3, 5, 6], [7, 8, 4, 6, 4]),
("Tailscale", [6, 2, 5, 0], [9, 9, 5, 4, 6]),
("Cloudflare WARP", [3, 4, 3, 5], [9, 8, 2, 4, 8]),
("Surfshark", [3, 3, 4, 5], [7, 8, 4, 5, 4]),
("Orchid", [3, 5, 3, 0], [6, 3, 3, 3, 3]),
("Mysterium", [3, 3, 3, 0], [6, 4, 6, 3, 4]),
("TunnelBear", [3, 2, 4, 0], [7, 6, 3, 5, 5]),
("Sentinel", [3, 3, 2, 0], [6, 4, 4, 6, 4]),
("IPVanish", [3, 2, 3, 0], [7, 7, 3, 4, 4]),
("Hotspot Shield", [3, 1, 2, 0], [7, 7, 2, 5, 5]),
("Hola", [1, 0, 0, 0], [5, 4, 5, 3, 7]),
]
WX = np.array([0.25, 0.35, 0.30, 0.10])
WY = np.array([0.30, 0.25, 0.20, 0.10, 0.15])
names = [p[0] for p in PRODUCTS]
XS = np.array([p[1] for p in PRODUCTS], float)
YS = np.array([p[2] for p in PRODUCTS], float)
UR, MV = names.index("URnetwork"), names.index("Mullvad")
OB, NY = names.index("Obscura"), names.index("NymVPN")
# sanity: every published total must recompute exactly (SCORES.md tables, product order above)
EXP_X = [7.30, 9.40, 7.50, 6.60, 6.00, 5.70, 5.10, 4.60, 4.20, 4.20, 3.90,
3.90, 3.70, 3.55, 3.50, 3.40, 2.70, 2.65, 2.40, 2.35, 1.70, 0.25]
EXP_Y = [6.20, 3.30, 4.00, 5.10, 6.20, 5.85, 4.95, 6.80, 5.75, 6.10, 6.70,
6.10, 7.25, 6.70, 6.00, 3.90, 4.90, 5.45, 4.80, 5.45, 5.50, 4.85]
assert np.allclose(XS @ WX, EXP_X) and np.allclose(YS @ WY, EXP_Y)
def ranks(scores): # rank = 1 + #{strictly greater}
return 1 + (scores[:, None, :] > scores[:, :, None]).sum(axis=2)
def q(a, p):
return np.quantile(a, p, method="nearest")
def sim_A(rng, subs, w, chunk=50_000):
R, S = [], []
for start in range(0, N, chunk):
m = min(chunk, N - start)
sc = rng.dirichlet(np.ones(len(w)), size=m) @ subs.T
R.append(ranks(sc)); S.append(sc)
return np.vstack(R), np.vstack(S)
def sim_B(rng, subs, w, chunk=20_000):
R, S = [], []
for start in range(0, N, chunk):
m = min(chunk, N - start)
noisy = np.clip(subs[None] + rng.uniform(-1, 1, (m,) + subs.shape), 0, 10)
sc = noisy @ w
R.append(ranks(sc)); S.append(sc)
return np.vstack(R), np.vstack(S)
rng = np.random.default_rng(SEED) # draw order matters for exact reproduction:
rAX, sAX = sim_A(rng, XS, WX) # 1) test 1, X axis
rAY, sAY = sim_A(rng, YS, WY) # 2) test 1, Y axis
rBX, sBX = sim_B(rng, XS, WX) # 3) test 2, X axis
rBY, sBY = sim_B(rng, YS, WY) # 4) test 2, Y axis
print("exact P(UR>MV | test 1) = 71/96 =", 71 / 96) # pre-rescore: 109/196
print("test 1: P(UR>MV on X) =", (sAX[:, UR] > sAX[:, MV]).mean())
print("test 2: P(UR>MV on X) =", (sBX[:, UR] > sBX[:, MV]).mean())
print("exact P(UR>Obscura | test 1) = 1 (pure X4 difference; pre-rescore: 5/6)")
print("test 1: P(UR>Obscura on X) =", (sAX[:, UR] > sAX[:, OB]).mean())
print("test 2: P(UR>Obscura on X) =", (sBX[:, UR] > sBX[:, OB]).mean())
print("exact P(UR>NymVPN | test 1) = 343/512 =", 343 / 512)
print("test 1: P(UR>NymVPN on X) =", (sAX[:, UR] > sAX[:, NY]).mean())
print("test 2: P(UR>NymVPN on X) =", (sBX[:, UR] > sBX[:, NY]).mean())
print("test 2: UR X score 5-95%:", q(sBX[:, UR], 0.05), q(sBX[:, UR], 0.95))
print("test 2: UR Y score 5-95%:", q(sBY[:, UR], 0.05), q(sBY[:, UR], 0.95))
print("test 2: UR Y < 5.00 in", int((sBY[:, UR] < 5).sum()), "of", N)
for label, r in (("AX", rAX), ("BX", rBX), ("AY", rAY), ("BY", rBY)):
print(label, "UR rank p5/med/p95:", int(q(r[:, UR], .05)), int(q(r[:, UR], .5)), int(q(r[:, UR], .95)))
print("| Product | X rank, weights | X rank, score noise | Y rank, weights | Y rank, score noise |")
print("|---|---|---|---|---|")
f = lambda r, j: f"{int(q(r[:, j], .05))}–{int(q(r[:, j], .95))} ({int(q(r[:, j], .5))})"
for j in np.argsort(-(XS @ WX), kind="stable"):
print(f"| {names[j]} | {f(rAX, j)} | {f(rBX, j)} | {f(rAY, j)} | {f(rBY, j)} |")