Skip to content

Commit 8faacf8

Browse files
fetch upstream and merge
2 parents f6b010d + 5c80870 commit 8faacf8

File tree

3 files changed

+3853
-0
lines changed

3 files changed

+3853
-0
lines changed
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
"""
2+
Experiments: Progressive Smoothing for Quantile Regression
3+
Reproduces GitHub issue #276 and validates the method
4+
"""
5+
import numpy as np
6+
import time
7+
from sklearn.datasets import make_regression
8+
from sklearn.linear_model import QuantileRegressor
9+
from skglm.experimental.quantile_huber import SmoothQuantileRegressor
10+
from skglm.experimental.quantile_regression import Pinball
11+
from skglm.penalties import L1
12+
from skglm import GeneralizedLinearEstimator
13+
from skglm.experimental.pdcd_ws import PDCD_WS
14+
import warnings
15+
from sklearn.exceptions import ConvergenceWarning
16+
from scipy import stats
17+
18+
19+
def pinball_loss(residuals, tau):
20+
return np.mean(residuals * (tau - (residuals < 0)))
21+
22+
23+
def test_pdcd_instability():
24+
"""Experiment 1: Reproduce PDCD-WS instability from GitHub issue #276"""
25+
print("Experiment 1: PDCD-WS Instability (GitHub #276)")
26+
print("-" * 45)
27+
28+
# Exact reproduction: n=1000, no scaling
29+
np.random.seed(42)
30+
X, y = make_regression(n_samples=1000, n_features=10, noise=0.1)
31+
32+
# PDCD-WS with Pinball (should fail to converge)
33+
datafit = Pinball(0.5)
34+
penalty = L1(alpha=0.1)
35+
solver = PDCD_WS(
36+
max_iter=500,
37+
max_epochs=500,
38+
tol=1e-2,
39+
warm_start=True,
40+
verbose=False)
41+
42+
start = time.time()
43+
with warnings.catch_warnings(record=True) as w:
44+
estimator = GeneralizedLinearEstimator(
45+
datafit=datafit, penalty=penalty, solver=solver)
46+
estimator.fit(X, y)
47+
did_not_converge = any(issubclass(warn.category, ConvergenceWarning)
48+
for warn in w)
49+
pdcd_time = time.time() - start
50+
51+
# Progressive Smoothing (should work)
52+
start = time.time()
53+
smooth_est = SmoothQuantileRegressor(
54+
quantile=0.5, alpha=0.1, delta_init=0.5,
55+
delta_final=0.01, n_deltas=5, verbose=False)
56+
smooth_est.fit(X, y)
57+
smooth_time = time.time() - start
58+
59+
print(
60+
f"PDCD-WS: {'DID NOT CONVERGE' if did_not_converge else 'CONVERGED'} "
61+
f"({pdcd_time:.3f}s)")
62+
print(f"Progressive: CONVERGED ({smooth_time:.3f}s)")
63+
64+
return did_not_converge # Return True if PDCD-WS did not converge
65+
66+
67+
def test_quantile_levels(n_runs=10):
68+
"""Experiment 2: Test across quantile levels with statistical validation"""
69+
print("\nExperiment 2: Quantile Level Validation")
70+
print("-" * 38)
71+
print("Coverage: % of data points below prediction (should match τ)")
72+
print(f"Results averaged over {n_runs} runs")
73+
print("-" * 38)
74+
75+
taus = [0.1, 0.3, 0.5, 0.7, 0.9] # Include extreme quantiles
76+
results = []
77+
78+
print(f"{'τ':<4} {'Progressive':<10} {'Sklearn':<10} {'Speedup':<8} "
79+
f"{'Coverage (P)':<12} {'Coverage (S)':<12} {'p-value':<8}")
80+
print("-" * 75)
81+
82+
for tau in taus:
83+
# Store results for each run
84+
smooth_times = []
85+
sk_times = []
86+
smooth_coverages = []
87+
sk_coverages = []
88+
89+
for _ in range(n_runs):
90+
# Generate new data for each run
91+
X, y = make_regression(n_samples=800, n_features=15, noise=0.1,
92+
random_state=None) # Different seed each time
93+
94+
# Progressive Smoothing
95+
start = time.time()
96+
smooth_est = SmoothQuantileRegressor(
97+
quantile=tau, alpha=0.1, delta_init=0.5,
98+
delta_final=0.01, n_deltas=5, verbose=False)
99+
smooth_est.fit(X, y)
100+
smooth_time = time.time() - start
101+
smooth_pred = smooth_est.predict(X)
102+
smooth_coverage = np.mean(y <= smooth_pred)
103+
104+
# Sklearn
105+
start = time.time()
106+
sk_est = QuantileRegressor(quantile=tau, alpha=0.1)
107+
sk_est.fit(X, y)
108+
sk_time = time.time() - start
109+
sk_pred = sk_est.predict(X)
110+
sk_coverage = np.mean(y <= sk_pred)
111+
112+
smooth_times.append(smooth_time)
113+
sk_times.append(sk_time)
114+
smooth_coverages.append(smooth_coverage)
115+
sk_coverages.append(sk_coverage)
116+
117+
# Compute statistics
118+
smooth_time_mean = np.mean(smooth_times)
119+
sk_time_mean = np.mean(sk_times)
120+
smooth_coverage_mean = np.mean(smooth_coverages)
121+
sk_coverage_mean = np.mean(sk_coverages)
122+
speedup = sk_time_mean / smooth_time_mean
123+
124+
# Statistical test for coverage difference
125+
t_stat, p_value = stats.ttest_rel(smooth_coverages, sk_coverages)
126+
127+
print(
128+
f"{tau:<4.1f} {smooth_time_mean:<10.3f} {sk_time_mean:<10.3f} "
129+
f"{speedup:<8.1f} {smooth_coverage_mean:<12.3f} "
130+
f"{sk_coverage_mean:<12.3f} {p_value:<8.3f}")
131+
132+
results.append((tau, smooth_time_mean, sk_time_mean, speedup,
133+
smooth_coverage_mean, sk_coverage_mean, p_value))
134+
135+
return results
136+
137+
138+
def test_scalability(n_runs=2):
139+
"""Experiment 3: Scalability comparison with statistical validation"""
140+
print("\nExperiment 3: Scalability Analysis")
141+
print("-" * 32)
142+
print(f"Results averaged over {n_runs} runs")
143+
print("-" * 32)
144+
145+
# Progressive smoothing parameters
146+
delta_init = 0.5
147+
delta_final = 0.05
148+
n_deltas = 5
149+
150+
# Range of problem sizes
151+
sizes = [
152+
(100, 10), # Small
153+
(1000, 100), # Medium
154+
(10000, 1000), # Large
155+
]
156+
results = []
157+
158+
# Format strings for consistent alignment
159+
size_fmt = "{:d}×{:d}"
160+
time_fmt = "{:.3f}"
161+
speedup_fmt = "{:.1f}"
162+
pval_fmt = "{:.3f}"
163+
obj_fmt = "{:.2e}"
164+
165+
print(f"{'Size':<10} {'Progressive':>10} {'Sklearn':>10} "
166+
f"{'Speedup':>8} {'p-value':>8} {'Obj (P-S)':>10}")
167+
print("-" * 60)
168+
print(f" Progressive uses {n_deltas} smoothing steps")
169+
print(f" from delta={delta_init:.2f} to {delta_final:.2f}")
170+
print("-" * 60)
171+
print(" Obj (P-S) > 0 means Progressive has worse objective (higher loss)")
172+
print("-" * 60)
173+
174+
for n, p in sizes:
175+
# Store results for each run
176+
smooth_times = []
177+
sk_times = []
178+
obj_diffs = []
179+
180+
for _ in range(n_runs):
181+
# Generate dense data
182+
X, y = make_regression(n_samples=n, n_features=p, noise=0.1,
183+
random_state=None)
184+
185+
# Progressive Smoothing - using the key innovation
186+
start = time.time()
187+
smooth_est = SmoothQuantileRegressor(
188+
quantile=0.5, alpha=0.1,
189+
delta_init=delta_init,
190+
delta_final=delta_final,
191+
n_deltas=n_deltas,
192+
max_iter=1000, tol=1e-4, verbose=False,
193+
solver="AndersonCD", fit_intercept=True)
194+
smooth_est.fit(X, y)
195+
smooth_time = time.time() - start
196+
smooth_pred = smooth_est.predict(X)
197+
smooth_obj = np.mean(np.abs(y - smooth_pred))
198+
199+
# Sklearn
200+
start = time.time()
201+
sk_est = QuantileRegressor(quantile=0.5, alpha=0.1)
202+
sk_est.fit(X, y)
203+
sk_time = time.time() - start
204+
sk_pred = sk_est.predict(X)
205+
sk_obj = np.mean(np.abs(y - sk_pred))
206+
207+
smooth_times.append(smooth_time)
208+
sk_times.append(sk_time)
209+
# Positive means Progressive has worse objective (higher loss)
210+
obj_diffs.append(smooth_obj - sk_obj)
211+
212+
# Compute statistics
213+
smooth_time_mean = np.mean(smooth_times)
214+
sk_time_mean = np.mean(sk_times)
215+
# Speedup > 1 means Progressive is faster
216+
speedup = sk_time_mean / smooth_time_mean
217+
obj_diff_mean = np.mean(obj_diffs)
218+
219+
# Statistical test for timing difference
220+
t_stat, p_value = stats.ttest_rel(smooth_times, sk_times)
221+
222+
# Print results
223+
size_str = size_fmt.format(n, p)
224+
smooth_str = time_fmt.format(smooth_time_mean)
225+
sk_str = time_fmt.format(sk_time_mean)
226+
speedup_str = speedup_fmt.format(speedup)
227+
pval_str = pval_fmt.format(p_value)
228+
obj_str = obj_fmt.format(obj_diff_mean)
229+
230+
print(f"{size_str:<10} {smooth_str:>10} {sk_str:>10} "
231+
f"{speedup_str:>8} {pval_str:>8} {obj_str:>10}")
232+
233+
results.append((n, p, smooth_time_mean, sk_time_mean, speedup,
234+
p_value, obj_diff_mean))
235+
236+
return results
237+
238+
239+
def main():
240+
"""Run all experiments and print results summary"""
241+
print("Progressive Smoothing for Quantile Regression")
242+
print("=" * 65)
243+
244+
# Run experiments
245+
instability_reproduced = test_pdcd_instability()
246+
quantile_results = test_quantile_levels()
247+
scale_results = test_scalability()
248+
249+
# Print summary
250+
print("\n" + "=" * 65)
251+
print("Summary")
252+
print("=" * 65)
253+
254+
status = 'Reproduced' if instability_reproduced else 'Not observed'
255+
print(f"PDCD-WS instability: {status}")
256+
257+
# Compute average speedup only for statistically significant results
258+
significant_speedups = [r[3] for r in quantile_results if r[6] < 0.05]
259+
avg_speedup = np.mean(significant_speedups) if significant_speedups else 0
260+
261+
significant_scale_speedups = [r[4] for r in scale_results if r[5] < 0.05]
262+
max_speedup = max(significant_scale_speedups) if significant_scale_speedups else 0
263+
264+
print(f"Average speedup (significant results only): {avg_speedup:.1f}")
265+
print(f"Maximum speedup (significant results only): {max_speedup:.1f}")
266+
print(f"Largest problem solved: {scale_results[-1][0]}×{scale_results[-1][1]}")
267+
268+
print("\nProgressive smoothing performs best when:")
269+
print("PDCD-WS fails to converge (n≥1000)")
270+
print(f"Extreme quantiles (τ≤0.3 or τ≥0.7): {avg_speedup:.1f} times faster")
271+
print(f"Large-scale problems: up to {max_speedup:.1f} times faster")
272+
273+
274+
if __name__ == "__main__":
275+
main()

0 commit comments

Comments
 (0)