問題
3人の学生に対して,補習を行ったところ,点数の差($d=$ 補習後 − 補習前)が次のようになった。
$D:\ -15,\ 6,\ 11$
符号検定(sign test)を説明する。本検定の帰無仮説は「分布 $D$ の中央値 $=0$」である。本例では補習の有効性を示したいので,対立仮説が「分布 $D$ の中央値 $>0$」の片側検定を考える。$D$ の分布の対称性を仮定する必要はない。0 の値をとらないサンプルサイズ $n$ に対して,差 $d$ の値が正になった個数 $T_+$ を検定統計量とする。帰無仮説が正しいとき,$T_+$ は二項分布 $ \mathrm{Bin}(n, 0.5) $ に従うと考えることができる。
符号検定は $d_i$ が独立で、かつ $P(d_i=0)=0$(連続)を仮定する。$0$ が混じるときは除外し $n^*=$#${d_i\neq 0}$ に置き換える。
本例では$n=3$ である。つまり,$T_+$ は二項分布 $ \mathrm{Bin}(n, 0.5) $ に従うと考える。$t_{+}=2$ より,符号検定片側 $P$-値は,$P(T_{+}\ge 2)=(_3C_2+_3C_3)\times(0.5)^3=4\times(0.5)^3\approx0.5$ となる。
n=3の符号検定を測度論で考える。
標本空間は$\Omega=\lbrace-1,+1 \rbrace^{n}={(\omega_1,\ldots,\omega_{n})}$という順序付きベクトルの集合だ。成分 $i$ は「$i$ 番目の観測(0 を除外し固定した順)」に対応する。
$n=3$ のとき $\Omega$ は $2^3=8$ 個。$T_+=2$ を満たすのは $(+,+,-),(+,-,+),(-,+,+)$ の 3 通りで、$\binom{3}{2}=3$。順序の違いは異なる $\omega$ だが、$T_+$ の値としては同一に束ねられる。
観測の符号を $\omega=(\omega_1,\omega_2,\omega_3)$、各成分 $\omega_i\in{-1,+1}$ とする。
順序付きなので標本空間は
$$
\begin{align*}
\Omega={-1,+1}^3
={(-1,-1,-1),,(-1,-1,+1),,(-1,+1,-1),,(-1,+1,+1),\
(+1,-1,-1),,(+1,-1,+1),,(+1,+1,-1),,(+1,+1,+1)}.
\end{align*}
$$
要素数は $|\Omega|=2^3=8$。
σ加法族の列挙の仕方
有限集合ではボレル族=冪集合なので
$$
\mathcal F=2^{\Omega}={A:\ A\subseteq\Omega},\qquad |\mathcal F|=2^{|\Omega|}=2^8=256.
$$
すべての部分集合を含む(空集合と $\Omega$ 自身を含み、補集合・可算和に閉)。
完全列挙は $256$ 個になりますが、サイズ別には
#$ {A\in\mathcal F:\ |A|=k}=\binom{8}{k}\quad(k=0,\dots,8).$
$\mathcal F=2^\Omega$ は 256 個の部分集合から成り、そのうち 符号検定で使う $\sigma(T_+)$ は $T_+\in{0,1,2,3}$ の冪集合に縮約して 16 個ある。
大きさ0:$\ \emptyset$
大きさ1:${0},{1},{2},{3}$
大きさ2:${0,1},{0,2},{0,3},{1,2},{1,3},{2,3}$
大きさ3:${0,1,2},{0,1,3},{0,2,3},{1,2,3}$
大きさ4:${0,1,2,3}$
対応する事象はすべて ${T_+\in A}$($A$ は上のいずれか)である。
$\Omega$ 上での原子(大きさ1)は
$$
\begin{aligned}
A_0&=T_+^{-1}({0})={(-,-,-)},\
A_1&=T_+^{-1}({1})={(+,-,-),(-,+,-),(-,-,+)},\
A_2&=T_+^{-1}({2})={(+,+,-),(+,-,+),(-,+,+)},\
A_3&=T_+^{-1}({3})={(+,+,+)},
\end{aligned}
$$
で、これを組み合わせればよい。たとえば、大きさ2の${0,1}={(-,-,-),(+,-,-),(-,+,-),(-,-,+)}$となる。
全事象は大きさ4で、単なる累積分布に必要な鎖は、
$$
{0}\subset{0,1}\subset{0,1,2}\subset{0,1,2,3}
$$
と記述できる。冪集合が16個必要なわけは、$\sigma$加法族の閉性(補集合・可算和に閉じる最小の入れ物)だ。区間型 ${0,1}$ や ${1,2}$ だけでなく、${0,2}$・${1,3}$(偶奇)など非区間事象も含めて整合的に確率を定義するため、結果として $2^{{0,1,2,3}}$ の 16 集合になる。
σ加法族(σ-代数)とは
集合 $\Omega$ 上の部分集合族 $\mathcal F\subseteq 2^\Omega$ が次を満たすとき,$\mathcal F$ をσ加法族(σ-代数)という。
- $\Omega\in\mathcal F$
- $A\in\mathcal F \Rightarrow A^{\mathrm c}\in\mathcal F$
- $A_1,A_2,\ldots\in\mathcal F \Rightarrow \bigcup_{k=1}^\infty A_k\in\mathcal F$
(2,3 から可算交わりや差集合にも閉じる。)
生成された σ加法族
与えられた集合族 $\mathcal C$ を含む最小の σ加法族を
$\sigma(\mathcal C)$
と書く(生成 σ加法族)。例:実数のボレル σ加法族は
$\mathcal B(\mathbb R)=\sigma{(a,b):a<b}.$
確率測度(帰無仮説下)
帰無仮説(符号対称・独立)では各点確率は一様:
$$
\mathbb P({\omega})=\tfrac{1}{8}\quad(\omega\in\Omega).
$$
検定統計量と可測性
正の個数
$T_+(\omega)=$#$ {i: \omega_i=+1}\in{0,1,2,3}$
は $\mathcal F$–可測。各値の逆像(事象)を完全列挙すると
$$
\begin{align*}
T_+^{-1}(0)&={(-1,-1,-1)},\
T_+^{-1}(1)&={(+1,-1,-1),(-1,+1,-1),(-1,-1,+1)},\
T_+^{-1}(2)&={(+1,+1,-1),(+1,-1,+1),(-1,+1,+1)},\
T_+^{-1}(3)&={(+1,+1,+1)}.
\end{align*}
$$
よって $$\mathbb P(T_+=k)=\frac{\binom{3}{k}}{8}\ (k=0,1,2,3),\quad \mathbb P(T_+\ge2)=\frac{\binom{3}{2}+\binom{3}{3}}{8}=\tfrac12.$$
ゆえに分布は
$$
\mathbb P(T_+=k)=\frac{\binom{3}{k}}{8}\quad(k=0,1,2,3).
$$
置換不偏性
同じ $T_+$ を与える並べ替えは等確率、$p$ 値はその軌道の総和として書ける。
片側 $P$ 値
「中央値 $0$」で $t_+=2$ を観測したなら
$$
P=\mathbb P(T_+\ge2)=\frac{\binom{3}{2}+\binom{3}{3}}{8}=\frac{3+1}{8}=\tfrac12.
$$
まとめ
+ $d_i\neq0$ のみ用い $n^\ast$ 個とする。
+ 写像
$S:\mathbb R^{n^\ast}\to{-1,+1}^{n^\ast},\quad S(x)=(\operatorname{sgn}x_1,\dots,\operatorname{sgn}x_{n^\ast})$
を可測とする。
+ $H_0$(中央値 $=0$、連続)下では $P_0\circ S^{-1}$ は ${-1,+1}^{n^\ast}$ 上の一様測度で、
$T_+(\omega)=\sum_{i=1}^{n^\ast}1_{{\omega_i=+1}}\quad\Rightarrow\quad P_0(T_+=k)=\binom{n^\ast}{k}2^{-n^\ast}.$
+ したがって片側 $p$ 値は $P_0(T_+\ge t_+^{\text{obs}})$、両側は $2\min{P_0(T_+\ge t_+^{\text{obs}}),,P_0(T_+\le t_+^{\text{obs}})}$。
正規分布とコーシー分布で試してみた。
from math import comb
import numpy as np
def sign_test(x, alternative="two-sided"):
"""
Exact sign test.
Parameters
----------
x : array-like
Sample of differences.
alternative : {"two-sided", "greater", "less"}
H1 options: "greater" median > 0, "less" median < 0, "two-sided".
Returns
-------
result : dict with keys
n_star : number of nonzero observations used
t_plus : count of positives
pvalue : exact p-value
"""
x = np.asarray(x)
x_nz = x[x != 0]
n_star = x_nz.size
if n_star == 0:
raise ValueError("All observations are zero; sign test not defined.")
t_plus = int(np.sum(x_nz > 0))
# exact binomial tail with p=0.5
def tail_ge(k, n): # P(T >= k)
return sum(comb(n, j) for j in range(k, n + 1)) / (2 ** n)
def tail_le(k, n): # P(T <= k)
return sum(comb(n, j) for j in range(0, k + 1)) / (2 ** n)
if alternative == "greater":
p = tail_ge(t_plus, n_star)
elif alternative == "less":
p = tail_le(t_plus, n_star)
elif alternative == "two-sided":
p = 2 * min(tail_ge(t_plus, n_star), tail_le(t_plus, n_star))
p = min(p, 1.0) # cap at 1 for discrete distributions
else:
raise ValueError("alternative must be 'two-sided', 'greater', or 'less'")
return {"n_star": n_star, "t_plus": t_plus, "pvalue": p}
# ------------------------------
# Example run with 100 variables
# ------------------------------
rng = np.random.default_rng(12345)
mu = 0.0 # try 0.3 to simulate an improvement
sigma = 1.0
n = 100
d = rng.normal(loc=mu, scale=sigma, size=n)
res_greater = sign_test(d, alternative="greater")
res_twosided = sign_test(d, alternative="two-sided")
print("Generated n=%.d observations (Normal(mu=%.2f, sigma=%.2f))" % (n,mu, sigma))
print("Nonzeros used (n*):", res_greater["n_star"])
print("T+ (positives): ", res_greater["t_plus"])
print("One-sided p (median > 0):", res_greater["pvalue"])
print("Two-sided p:", res_twosided["pvalue"])
Generated n=100 observations (Normal(mu=0.00, sigma=1.00))
Nonzeros used (n*): 100
T+ (positives): 50
One-sided p (median > 0): 0.5397946186935894
Two-sided p: 1.0
mu = 0.3 # try 0.3 to simulate an improvement
sigma = 1.0
n = 100
d = rng.normal(loc=mu, scale=sigma, size=n)
res_greater = sign_test(d, alternative="greater")
res_twosided = sign_test(d, alternative="two-sided")
print("Generated n=%.d observations (Normal(mu=%.2f, sigma=%.2f))" % (n,mu, sigma))
print("Nonzeros used (n*):", res_greater["n_star"])
print("T+ (positives): ", res_greater["t_plus"])
print("One-sided p (median > 0):", res_greater["pvalue"])
print("Two-sided p:", res_twosided["pvalue"])
Generated n=100 observations (Normal(mu=0.30, sigma=1.00))
Nonzeros used (n*): 100
T+ (positives): 62
One-sided p (median > 0): 0.010489367838925859
Two-sided p: 0.020978735677851718
mu = 0.0 # try 0.3 to simulate an improvement
sigma = 1.0
n = 10000
d = rng.normal(loc=mu, scale=sigma, size=n)
res_greater = sign_test(d, alternative="greater")
res_twosided = sign_test(d, alternative="two-sided")
print("Generated n=%.d observations (Normal(mu=%.2f, sigma=%.2f))" % (n,mu, sigma))
print("Nonzeros used (n*):", res_greater["n_star"])
print("T+ (positives): ", res_greater["t_plus"])
print("One-sided p (median > 0):", res_greater["pvalue"])
print("Two-sided p:", res_twosided["pvalue"])
Generated n=10000 observations (Normal(mu=0.00, sigma=1.00))
Nonzeros used (n*): 10000
T+ (positives): 5024
One-sided p (median > 0): 0.31917859905162393
Two-sided p: 0.6383571981032479
mu = 0.3 # try 0.3 to simulate an improvement
sigma = 1.0
n = 10000
d = rng.normal(loc=mu, scale=sigma, size=n)
res_greater = sign_test(d, alternative="greater")
res_twosided = sign_test(d, alternative="two-sided")
print("Generated n=%.d observations (Normal(mu=%.2f, sigma=%.2f))" % (n,mu, sigma))
print("Nonzeros used (n*):", res_greater["n_star"])
print("T+ (positives): ", res_greater["t_plus"])
print("One-sided p (median > 0):", res_greater["pvalue"])
print("Two-sided p:", res_twosided["pvalue"])
Generated n=10000 observations (Normal(mu=0.30, sigma=1.00))
Nonzeros used (n*): 10000
T+ (positives): 6261
One-sided p (median > 0): 4.917207164489503e-142
Two-sided p: 9.834414328979006e-142
def run_cauchy(n=100, x0=0.0, gamma=1.0, seed=20250813):
rng = np.random.default_rng(seed)
d = x0 + gamma * rng.standard_cauchy(size=n)
# Use the sign_test defined earlier in the session
res_greater = sign_test(d, alternative="greater")
res_twosided = sign_test(d, alternative="two-sided")
num_pos = int(np.sum(d > 0))
num_neg = int(np.sum(d < 0))
num_zero = int(np.sum(d == 0))
q05, q50, q95 = np.quantile(d, [0.05, 0.5, 0.95])
print(f"Cauchy data (n={n}, x0={x0}, gamma={gamma}, seed={seed})")
print(f"Sign summary -> +:{num_pos} -:{num_neg} 0:{num_zero}")
print(f"Quantiles (5%, 50%, 95%): {q05:.3f}, {q50:.3f}, {q95:.3f}")
print(f"T+ (positives) = {res_greater['t_plus']} | n* = {res_greater['n_star']}")
print("One-sided p (median > 0):", res_greater["pvalue"])
print("Two-sided p:", res_twosided["pvalue"])
np.set_printoptions(precision=3, suppress=True)
print("First 10 values:", d[:10])
print("-"*72)
# --- Example 1: under H0 (x0=0) ---
run_cauchy(n=100, x0=0.0, gamma=1.0, seed=20250813)
# --- Example 2: under H1 (median shifted to the right) ---
run_cauchy(n=100, x0=0.3, gamma=1.0, seed=20250813)
Cauchy data (n=100, x0=0.0, gamma=1.0, seed=20250813)
Sign summary -> +:50 -:50 0:0
Quantiles (5%, 50%, 95%): -7.121, -0.023, 5.242
T+ (positives) = 50 | n* = 100
One-sided p (median > 0): 0.5397946186935894
Two-sided p: 1.0
First 10 values: [-1.934 0.917 -0.809 -1.404 0.753 -3.015 1.315 0.517 0.307 -0.659]
Cauchy data (n=100, x0=0.3, gamma=1.0, seed=20250813)
Sign summary -> +:55 -:45 0:0
Quantiles (5%, 50%, 95%): -6.821, 0.277, 5.542
T+ (positives) = 55 | n* = 100
One-sided p (median > 0): 0.18410080866334813
Two-sided p: 0.36820161732669626
First 10 values: [-1.634 1.217 -0.509 -1.104 1.053 -2.715 1.615 0.817 0.607 -0.359]
# --- Example 1: under H0 (x0=0) ---
run_cauchy(n=10000, x0=0.0, gamma=1.0, seed=20250813)
# --- Example 2: under H1 (median shifted to the right) ---
run_cauchy(n=10000, x0=0.3, gamma=1.0, seed=20250813)
Cauchy data (n=100, x0=0.0, gamma=1.0, seed=20250813)
Sign summary -> +:50 -:50 0:0
Quantiles (5%, 50%, 95%): -7.121, -0.023, 5.242
T+ (positives) = 50 | n* = 100
One-sided p (median > 0): 0.5397946186935894
Two-sided p: 1.0
First 10 values: [-1.934 0.917 -0.809 -1.404 0.753 -3.015 1.315 0.517 0.307 -0.659]
Cauchy data (n=100, x0=0.3, gamma=1.0, seed=20250813)
Sign summary -> +:55 -:45 0:0
Quantiles (5%, 50%, 95%): -6.821, 0.277, 5.542
T+ (positives) = 55 | n* = 100
One-sided p (median > 0): 0.18410080866334813
Two-sided p: 0.36820161732669626
First 10 values: [-1.634 1.217 -0.509 -1.104 1.053 -2.715 1.615 0.817 0.607 -0.359]
符号検定は非対称の分布に関しても割と機能するので、相場師のために道具だ。ここでは正規分布よりもコーシー分布でいい結果が出ている。
参考
Python3ではじめるシステムトレード【第2版】環境構築と売買戦略
「画像をクリックしていただくとpanrollingのホームページから書籍を購入していただけます。
「画像をクリックしていただくとpanrollingのホームページから書籍を購入していただけます。」