@@ -73,6 +73,7 @@ from scipy.integrate import quad
7373from scipy.stats import beta
7474from collections import namedtuple
7575import pandas as pd
76+ import scipy as sp
7677```
7778
7879This lecture uses ideas studied in {doc}` the lecture on likelihood ratio processes<likelihood_ratio_process> ` and {doc}` the lecture on Bayesian learning<likelihood_bayes> ` .
@@ -327,8 +328,7 @@ f0 = create_beta_density(1, 1)
327328f1 = create_beta_density(9, 9)
328329grid = np.linspace(0, 1, 50)
329330
330- fig, ax = plt.subplots(figsize=(10, 8))
331- ax.set_title("Original Distributions")
331+ fig, ax = plt.subplots()
332332ax.plot(grid, f0(grid), lw=2, label="$f_0$")
333333ax.plot(grid, f1(grid), lw=2, label="$f_1$")
334334ax.legend()
@@ -538,7 +538,10 @@ def run_sprt_simulation(a0, b0, a1, b1, α, β, N, seed):
538538 true_f0 = (i % 2 == 0)
539539 truth_h0[i] = true_f0
540540
541- n, accept_f0 = sprt_single_run(a0, b0, a1, b1, logA, logB, true_f0, seed + i)
541+ n, accept_f0 = sprt_single_run(
542+ a0, b0, a1, b1,
543+ logA, logB,
544+ true_f0, seed + i)
542545 stopping_times[i] = n
543546 decisions_h0[i] = accept_f0
544547
@@ -555,8 +558,10 @@ def run_sprt(params):
555558 truth_h0_bool = truth_h0.astype(bool)
556559 decisions_h0_bool = decisions_h0.astype(bool)
557560
558- type_I = np.sum(truth_h0_bool & ~decisions_h0_bool) / np.sum(truth_h0_bool)
559- type_II = np.sum(~truth_h0_bool & decisions_h0_bool) / np.sum(~truth_h0_bool)
561+ type_I = np.sum(truth_h0_bool & ~decisions_h0_bool) \
562+ / np.sum(truth_h0_bool)
563+ type_II = np.sum(~truth_h0_bool & decisions_h0_bool) \
564+ / np.sum(~truth_h0_bool)
560565
561566 return {
562567 'stopping_times': stopping_times,
@@ -571,8 +576,8 @@ params = SPRTParams(α=0.05, β=0.10, a0=2, b0=5, a1=5, b1=2, N=20000, seed=1)
571576results = run_sprt(params)
572577
573578print(f"Average stopping time: {results['stopping_times'].mean():.2f}")
574- print(f"Empirical type I error: {results['type_I']:.3f} (target = {params.α})")
575- print(f"Empirical type II error: {results['type_II']:.3f} (target = {params.β})")
579+ print(f"Empirical type I error: {results['type_I']:.3f} (target = {params.α})")
580+ print(f"Empirical type II error: {results['type_II']:.3f} (target = {params.β})")
576581```
577582
578583As anticipated in the passage above in which Wald discussed the quality of
@@ -597,7 +602,7 @@ def compute_wald_thresholds(α, β):
597602 return A, B, np.log(A), np.log(B)
598603
599604def plot_sprt_results(results, params, title=""):
600- """Reusable function to plot SPRT results."""
605+ """Plot SPRT results."""
601606 fig, axes = plt.subplots(1, 3, figsize=(20, 6))
602607
603608 # Distribution plots
@@ -656,8 +661,10 @@ def plot_confusion_matrix(results, ax):
656661
657662 for i in range(2):
658663 for j in range(2):
659- percent = confusion_data[i, j] / row_totals[i, 0] if row_totals[i, 0] > 0 else 0
660- color = 'white' if confusion_data[i, j] > confusion_data.max() * 0.5 else 'black'
664+ percent = confusion_data[i, j] / row_totals[i, 0] \
665+ if row_totals[i, 0] > 0 else 0
666+ color = 'white' if confusion_data[i, j] > confusion_data.max() * 0.5 \
667+ else 'black'
661668 ax.text(j, i, f'{confusion_data[i, j]}\n({percent:.1%})',
662669 ha="center", va="center", color=color, fontweight='bold',
663670 fontsize=14)
@@ -765,7 +772,7 @@ for a0, b0, a1, b1 in param_comb:
765772 param_list.append((a0, b0, a1, b1))
766773
767774# Create the plot
768- fig, ax = plt.subplots(figsize=(6, 6) )
775+ fig, ax = plt.subplots()
769776
770777scatter = ax.scatter(js_dists, mean_stopping_times,
771778 s=80, alpha=0.7, linewidth=0.5)
@@ -1009,6 +1016,8 @@ In the two exercises below, please try to rewrite the entire SPRT suite in this
10091016
10101017In the first exercise, we apply the sequential probability ratio test to distinguish two models generated by 3-state Markov chains
10111018
1019+ (For a review on likelihood ratio processes for Markov chains, see [this section](lrp_markov).)
1020+
10121021Consider distinguishing between two 3-state Markov chain models using Wald's sequential probability ratio test.
10131022
10141023You have competing hypotheses about the transition probabilities:
@@ -1049,6 +1058,12 @@ The test stops when:
10491058:class: dropdown
10501059```
10511060
1061+ Below is one solution to the exercise.
1062+
1063+ In the lecture, we write the code more verbosely to illustrate the concepts clearly.
1064+
1065+ In the code below, we simplified some of the code structure for a shorter presentation.
1066+
10521067``` {code-cell} ipython3
10531068MarkovSPRTParams = namedtuple('MarkovSPRTParams',
10541069 ['α', 'β', 'P_0', 'P_1', 'N', 'seed'])
@@ -1076,7 +1091,8 @@ def simulate_markov_chain(P, pi_0, T, seed):
10761091 return path
10771092
10781093@njit
1079- def markov_sprt_single_run(P_0, P_1, pi_0, pi_1, logA, logB, true_P, true_pi, seed):
1094+ def markov_sprt_single_run(P_0, P_1, pi_0, pi_1,
1095+ logA, logB, true_P, true_pi, seed):
10801096 """Run single SPRT for Markov chains."""
10811097 max_n = 10000
10821098 path = simulate_markov_chain(true_P, true_pi, max_n, seed)
@@ -1137,14 +1153,18 @@ P_1 = np.array([[0.5, 0.3, 0.2],
11371153 [0.2, 0.6, 0.2],
11381154 [0.2, 0.2, 0.6]])
11391155
1140- params_markov = MarkovSPRTParams(α=0.05, β=0.10, P_0=P_0, P_1=P_1, N=1000, seed=42)
1156+ params_markov = MarkovSPRTParams(α=0.05, β=0.10,
1157+ P_0=P_0, P_1=P_1, N=1000, seed=42)
11411158results_markov = run_markov_sprt(params_markov)
11421159
11431160plot_confusion_matrix = lambda results, ax: None
11441161fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 5))
11451162
1146- ax1.hist(results_markov['stopping_times'], bins=50, color="steelblue", alpha=0.8)
1147- ax1.set_title("Stopping Times"), ax1.set_xlabel("n"), ax1.set_ylabel("Frequency")
1163+ ax1.hist(results_markov['stopping_times'],
1164+ bins=50, color="steelblue", alpha=0.8)
1165+ ax1.set_title("Stopping Times")
1166+ ax1.set_xlabel("n")
1167+ ax1.set_ylabel("Frequency")
11481168
11491169# Confusion matrix (reusing pattern from lecture)
11501170f0_c = np.sum(results_markov['truth_h0'] & results_markov['decisions_h0'])
@@ -1154,20 +1174,23 @@ f1_i = np.sum(~results_markov['truth_h0'] & results_markov['decisions_h0'])
11541174
11551175confusion_data = np.array([[f0_c, f0_i], [f1_i, f1_c]])
11561176ax2.imshow(confusion_data, cmap='Blues')
1157- ax .set_title('Confusion Matrix')
1158- ax .set_xticks([0, 1])
1159- ax .set_xticklabels(['Accept $H_0$', 'Reject $H_0$'])
1160- ax .set_yticks([0, 1])
1161- ax .set_yticklabels(['True $P^{(0)}$', 'True $P^{(1)}$'])
1177+ ax2 .set_title('Confusion Matrix')
1178+ ax2 .set_xticks([0, 1])
1179+ ax2 .set_xticklabels(['Accept $H_0$', 'Reject $H_0$'])
1180+ ax2 .set_yticks([0, 1])
1181+ ax2 .set_yticklabels(['True $P^{(0)}$', 'True $P^{(1)}$'])
11621182
11631183row_totals = confusion_data.sum(axis=1, keepdims=True)
11641184
11651185for i in range(2):
11661186 for j in range(2):
1167- percent = confusion_data[i, j] / row_totals[i, 0] if row_totals[i, 0] > 0 else 0
1168- color = 'white' if confusion_data[i, j] > confusion_data.max() * 0.5 else 'black'
1169- ax.text(j, i, f'{confusion_data[i, j]}\n({percent:.1%})',
1170- ha="center", va="center", color=color, fontweight='bold')
1187+ percent = confusion_data[i, j] / row_totals[i, 0] \
1188+ if row_totals[i, 0] > 0 else 0
1189+ color = 'white' if confusion_data[i, j] > confusion_data.max() * 0.5 \
1190+ else 'black'
1191+ ax2.text(j, i, f'{confusion_data[i, j]}\n({percent:.1%})',
1192+ ha="center", va="center", color=color, fontweight='bold',
1193+ fontsize=14)
11711194
11721195plt.tight_layout()
11731196plt.show()
@@ -1182,6 +1205,8 @@ plt.show()
11821205
11831206In this exercise, apply Wald's sequential test to distinguish between two VAR(1) models with different dynamics and noise structures.
11841207
1208+ For a review of the likelihood ratio process with VAR models, see {doc}`likelihood_var`.
1209+
11851210Given VAR models under each hypothesis:
11861211- $H_0$: $x_{t+1} = A^{(0)} x_t + C^{(0)} w_{t+1}$
11871212- $H_1$: $x_{t+1} = A^{(1)} x_t + C^{(1)} w_{t+1}$
@@ -1210,17 +1235,17 @@ Tasks:
12101235:class: dropdown
12111236```
12121237
1213- ``` {code-cell} ipython3
1214- import scipy as sc
1238+ Below is one solution to the exercise
12151239
1240+ ``` {code-cell} ipython3
12161241VARSPRTParams = namedtuple('VARSPRTParams',
12171242 ['α', 'β', 'A_0', 'C_0', 'A_1', 'C_1', 'N', 'seed'])
12181243
12191244def create_var_model(A, C):
12201245 """Create VAR model."""
12211246 μ_0 = np.zeros(A.shape[0])
12221247 CC = C @ C.T
1223- Σ_0 = sc .linalg.solve_discrete_lyapunov(A, CC)
1248+ Σ_0 = sp .linalg.solve_discrete_lyapunov(A, CC)
12241249
12251250 CC_inv = np.linalg.inv(CC + 1e-10 * np.eye(CC.shape[0]))
12261251 Σ_0_inv = np.linalg.inv(Σ_0 + 1e-10 * np.eye(Σ_0.shape[0]))
@@ -1246,14 +1271,16 @@ def var_log_likelihood(x_curr, x_prev, model, initial=False):
12461271 return -0.5 * (n * np.log(2 * np.pi) + model['log_det_CC'] +
12471272 diff @ model['CC_inv'] @ diff)
12481273
1249- def var_sprt_single_run(model_0, model_1, model_true, logA, logB, seed):
1250- """Single VAR SPRT run"""
1274+ def var_sprt_single_run(model_0, model_1, model_true,
1275+ logA, logB, seed):
1276+ """Single VAR SPRT run."""
12511277 np.random.seed(seed)
12521278 max_T = 500
12531279
12541280 # Generate VAR path
12551281 Σ_chol = np.linalg.cholesky(model_true['Σ_0'])
1256- x = model_true['μ_0'] + Σ_chol @ np.random.randn(len(model_true['μ_0']))
1282+ x = model_true['μ_0'] + Σ_chol @ np.random.randn(
1283+ len(model_true['μ_0']))
12571284
12581285 # Initial likelihood ratio
12591286 log_L = (var_log_likelihood(x, None, model_1, True) -
@@ -1299,8 +1326,10 @@ def run_var_sprt(params):
12991326 type_I = np.sum(truth_h0 & ~decisions_h0) / np.sum(truth_h0)
13001327 type_II = np.sum(~truth_h0 & decisions_h0) / np.sum(~truth_h0)
13011328
1302- return {'stopping_times': stopping_times, 'decisions_h0': decisions_h0,
1303- 'truth_h0': truth_h0, 'type_I': type_I, 'type_II': type_II}
1329+ return {'stopping_times': stopping_times,
1330+ 'decisions_h0': decisions_h0,
1331+ 'truth_h0': truth_h0,
1332+ 'type_I': type_I, 'type_II': type_II}
13041333
13051334# Run VAR SPRT
13061335A_0 = np.array([[0.8, 0.1],
@@ -1329,8 +1358,8 @@ ax2.bar(x - 0.2, [results_markov['type_I'], results_var['type_I']],
13291358 0.4, label='Type I', alpha=0.7)
13301359ax2.bar(x + 0.2, [results_markov['type_II'], results_var['type_II']],
13311360 0.4, label='Type II', alpha=0.7)
1332- ax2.axhline(y=0.05, linestyle='--', alpha=0.5)
1333- ax2.axhline(y=0.10, linestyle='--', alpha=0.5)
1361+ ax2.axhline(y=0.05, linestyle='--', alpha=0.5, color='C0' )
1362+ ax2.axhline(y=0.10, linestyle='--', alpha=0.5, color='C1' )
13341363ax2.set_xticks(x), ax2.set_xticklabels(['Markov', 'VAR'])
13351364ax2.legend(), plt.tight_layout(), plt.show()
13361365```
0 commit comments