Skip to content

quanttoolbox.svm

Python alternatives

Switch to sklearn.svm.SVC/SVR for standalone classification/regression — backed by libsvm/liblinear (compiled C), extremely well optimized and battle-tested. Already verified to match this module to 3+ decimal places. Keep this module only if the SVM needs to be composed with other constraints inside the same solve_qp/cvxpy optimization — that composability is the one thing sklearn's opaque solver can't offer.

svm.svm

quanttoolbox.svm.svm

Support Vector Machines: classification and regression, primal and dual formulations.

Ported from QuantToolBox/svm/{svm_classification_dual, svm_classification_primal,svm_regression_dual,svm_regression_primal}.m (QuantToolBox/theo/svm_*.m are byte-identical duplicates, not ported separately).

Consolidation notes:

Every one of the original four functions builds a hand-constructed QP (block matrices for slack variables, box bounds, one equality constraint) and calls MATLAB's quadprog directly. Since quanttoolbox.optim.quadprog.solve_qp already accepts arbitrary Q/R/equality/inequality/box arguments, each branch below is a direct, literal translation of the original's Q/R/constraint construction into a single solve_qp call -- no additional QP machinery is needed. Sign convention: MATLAB's quadprog(Q, f, ...) minimizes 0.5*x'Qx + f'x; solve_qp(Q, R, ...) minimizes 0.5*x'Qx - R'x, so every R passed to solve_qp below is the negation of the original's f.

MATLAB's global SVM_macheps is replaced by quanttoolbox.config.SVMConfig.

svm_classification_dual(y, x, c=None, loss='hinge', random_start=False, config=None)

SVM classification, dual (kernel-ready) formulation.

c=None (default): hard margin. loss="hinge" (default): standard soft-margin hinge loss (box-constrained dual, alpha in [0, c]). loss="squared_hinge": squared hinge loss (ridge-regularized dual, alpha in [0, inf)).

Original: svm/svm_classification_dual.m

Source code in src/quanttoolbox/svm/svm.py
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def svm_classification_dual(
    y: np.ndarray,
    x: np.ndarray,
    c: float | None = None,
    loss: str = "hinge",
    random_start: bool = False,
    config: SVMConfig | None = None,
) -> SVMClassificationResult:
    """SVM classification, dual (kernel-ready) formulation.

    c=None (default): hard margin. loss="hinge" (default): standard
    soft-margin hinge loss (box-constrained dual, alpha in [0, c]).
    loss="squared_hinge": squared hinge loss (ridge-regularized dual,
    alpha in [0, inf)).

    Original: svm/svm_classification_dual.m
    """
    if config is None:
        config = SVMConfig()

    y = np.asarray(y, dtype=float).flatten()
    x = np.asarray(x, dtype=float)
    n = x.shape[0]

    ydx = y[:, None] * x
    q = ydx @ ydx.T
    r = np.ones(n)
    a_eq = y[None, :]
    b_eq = np.array([0.0])

    hard_margin = c is None or (isinstance(c, float) and np.isnan(c))

    if hard_margin:
        lb, ub = np.zeros(n), None
    elif loss == "squared_hinge":
        assert c is not None
        q = q + np.eye(n) / (2 * c)
        lb, ub = np.zeros(n), None
    else:
        assert c is not None
        lb, ub = np.zeros(n), np.full(n, c)

    alpha = solve_qp(q, r, a_eq=a_eq, b_eq=b_eq, lb=lb, ub=ub)
    alpha = np.where(alpha >= config.macheps, alpha, 0.0)

    if not hard_margin and loss != "squared_hinge":
        near_c = np.abs(alpha - c) <= config.macheps
        alpha = np.where(near_c, c, alpha)
        sv = np.where((alpha > 0) & (alpha < c))[0]
    else:
        sv = np.where(alpha > 0)[0]

    beta = np.sum(alpha[:, None] * y[:, None] * x, axis=0)
    beta0 = float(np.mean(y[sv] - x[sv] @ beta))
    xi = np.maximum(0, 1 - y * (beta0 + x @ beta))
    xi = np.where(np.abs(xi) >= config.macheps, xi, 0.0)
    margin = float(1.0 / (beta @ beta))

    return SVMClassificationResult(
        beta0=beta0, beta=beta, xi=xi, margin=margin, alpha=alpha, support_vectors=sv
    )

svm_classification_primal(y, x, c=None, loss='hinge')

SVM classification, primal formulation.

Same c/loss semantics as svm_classification_dual.

Original: svm/svm_classification_primal.m

Source code in src/quanttoolbox/svm/svm.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def svm_classification_primal(
    y: np.ndarray, x: np.ndarray, c: float | None = None, loss: str = "hinge"
) -> SVMClassificationResult:
    """SVM classification, primal formulation.

    Same c/loss semantics as ``svm_classification_dual``.

    Original: svm/svm_classification_primal.m
    """
    y = np.asarray(y, dtype=float).flatten()
    x = np.asarray(x, dtype=float)
    n, k = x.shape

    hard_margin = c is None or (isinstance(c, float) and np.isnan(c))

    if hard_margin:
        q = np.zeros((k + 1, k + 1))
        q[1:, 1:] = np.eye(k)
        r = np.zeros(k + 1)
        c_ineq = -(y[:, None] * np.column_stack([np.ones(n), x]))
        d_ineq = -np.ones(n)
        theta = solve_qp(q, r, c_ineq=c_ineq, d_ineq=d_ineq)
        beta0, beta = theta[0], theta[1 : k + 1]
        xi = np.full(n, np.nan)

    elif loss == "squared_hinge":
        assert c is not None
        q = np.zeros((1 + k + n, 1 + k + n))
        q[1 : k + 1, 1 : k + 1] = np.eye(k)
        q[k + 1 :, k + 1 :] = 2 * c * np.eye(n)
        r = np.zeros(1 + k + n)
        c_block = y[:, None] * np.column_stack([np.ones(n), x])
        c_ineq = -np.hstack([c_block, np.eye(n)])
        d_ineq = -np.ones(n)
        lb = np.concatenate([np.full(1 + k, -np.inf), np.zeros(n)])
        theta = solve_qp(q, r, c_ineq=c_ineq, d_ineq=d_ineq, lb=lb)
        beta0, beta = theta[0], theta[1 : k + 1]
        xi = theta[k + 1 :]

    else:
        assert c is not None
        q = np.zeros((1 + k + n, 1 + k + n))
        q[1 : k + 1, 1 : k + 1] = np.eye(k)
        r = np.concatenate([np.zeros(k + 1), -c * np.ones(n)])
        c_ineq = -np.hstack([y[:, None], y[:, None] * x, np.eye(n)])
        d_ineq = -np.ones(n)
        lb = np.concatenate([np.full(k + 1, -np.inf), np.zeros(n)])
        theta = solve_qp(q, r, c_ineq=c_ineq, d_ineq=d_ineq, lb=lb)
        beta0, beta = theta[0], theta[1 : k + 1]
        xi = theta[k + 1 :]

    margin = float(1.0 / (beta @ beta))
    return SVMClassificationResult(beta0=float(beta0), beta=beta, xi=xi, margin=margin)

svm_regression_dual(y, x, c, epsilon=None, config=None)

SVM regression, dual (kernel-ready) formulation.

epsilon=None (default, or a negative value): Least-Squares SVM (ridge-regularized). epsilon=0: epsilon-SVM with epsilon-tube width 0 (box-constrained dual). epsilon>0: standard epsilon-insensitive SVM regression.

Original: svm/svm_regression_dual.m

Source code in src/quanttoolbox/svm/svm.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
def svm_regression_dual(
    y: np.ndarray,
    x: np.ndarray,
    c: float,
    epsilon: float | None = None,
    config: SVMConfig | None = None,
) -> SVMRegressionResult:
    """SVM regression, dual (kernel-ready) formulation.

    epsilon=None (default, or a negative value): Least-Squares SVM
    (ridge-regularized). epsilon=0: epsilon-SVM with epsilon-tube width 0
    (box-constrained dual). epsilon>0: standard epsilon-insensitive SVM
    regression.

    Original: svm/svm_regression_dual.m
    """
    if config is None:
        config = SVMConfig()

    y = np.asarray(y, dtype=float).flatten()
    x = np.asarray(x, dtype=float)
    n = x.shape[0]

    q = x @ x.T

    if epsilon is None or epsilon < 0:
        c_eff = np.finfo(np.float32).eps if c == 0 else c
        q = q + np.eye(n) / (2 * c_eff)
        r = y
        a_eq = np.ones((1, n))
        b_eq = np.array([0.0])

        alpha = solve_qp(q, r, a_eq=a_eq, b_eq=b_eq)
        beta = x.T @ alpha
        beta0 = float(np.mean(y - x @ beta))
        xi = y - beta0 - x @ beta
        alpha = np.where(np.abs(alpha) >= config.macheps, alpha, 0.0)

        return SVMRegressionResult(
            beta0=beta0, beta=beta, xi=xi, margin=float(1.0 / (beta @ beta)), alpha=alpha
        )

    if epsilon == 0:
        r = y
        a_eq = np.ones((1, n))
        b_eq = np.array([0.0])
        lb, ub = np.full(n, -c), np.full(n, c)

        delta = solve_qp(q, r, a_eq=a_eq, b_eq=b_eq, lb=lb, ub=ub)
        beta = x.T @ delta

        near_c = np.abs(delta - c) <= config.macheps
        delta = np.where(near_c, c, delta)
        near_neg_c = np.abs(delta + c) <= config.macheps
        delta = np.where(near_neg_c, -c, delta)

        u = y - x @ beta
        sv_mask = (delta > -c) & (delta < c)
        beta0 = float(np.mean(u[sv_mask])) if np.any(sv_mask) else 0.0

        u = y - beta0 - x @ beta
        xi_minus = np.where(delta == -c, np.maximum(-u, 0), 0.0)
        xi_plus = np.where(delta == c, np.maximum(u, 0), 0.0)
        xi = np.column_stack([xi_minus, xi_plus])

        return SVMRegressionResult(
            beta0=beta0, beta=beta, xi=xi, margin=float(1.0 / (beta @ beta)), alpha=delta
        )

    # standard epsilon-insensitive SVM regression
    q_big = np.block([[q, -q], [-q, q]])
    # the block structure [[Q,-Q],[-Q,Q]] is PSD but rank-deficient (rank
    # <= n, not 2n), which can make general QP solvers struggle to
    # converge; a small ridge regularization on the diagonal is a standard
    # fix and doesn't change the solution in any way that matters (the
    # true optimum is unaffected to numerical precision).
    q_big = q_big + 1e-8 * np.eye(2 * n)
    r_big = np.concatenate([-(y + epsilon), y - epsilon])
    a_eq = np.concatenate([np.ones(n), -np.ones(n)])[None, :]
    b_eq = np.array([0.0])
    lb, ub = np.zeros(2 * n), np.full(2 * n, c)

    alpha_stacked = solve_qp(q_big, r_big, a_eq=a_eq, b_eq=b_eq, lb=lb, ub=ub)
    alpha_minus, alpha_plus = alpha_stacked[:n], alpha_stacked[n:]
    alpha_minus = np.where(np.abs(alpha_minus) >= config.macheps, alpha_minus, 0.0)
    alpha_plus = np.where(np.abs(alpha_plus) >= config.macheps, alpha_plus, 0.0)
    alpha_minus = np.where(np.abs(alpha_minus - c) <= config.macheps, c, alpha_minus)
    alpha_plus = np.where(np.abs(alpha_plus - c) <= config.macheps, c, alpha_plus)

    beta = x.T @ (alpha_plus - alpha_minus)
    u_minus = y + epsilon - x @ beta
    u_plus = y - epsilon - x @ beta

    sv_minus = np.where((alpha_minus > 0) & (alpha_minus < c))[0]
    sv_plus = np.where((alpha_plus > 0) & (alpha_plus < c))[0]

    if sv_minus.size == 0 and sv_plus.size > 0:
        beta0 = float(np.mean(u_plus[sv_plus]))
    elif sv_plus.size == 0 and sv_minus.size > 0:
        beta0 = float(np.mean(u_minus[sv_minus]))
    elif sv_minus.size > 0 and sv_plus.size > 0:
        beta0 = float(np.mean(np.concatenate([u_minus[sv_minus], u_plus[sv_plus]])))
    else:
        beta0 = 0.0

    u_minus = y + epsilon - beta0 - x @ beta
    u_plus = y - epsilon - beta0 - x @ beta
    xi_minus = -((alpha_minus == c).astype(float) * u_minus)
    xi_plus = (alpha_plus == c) * u_plus
    xi = np.column_stack([xi_minus, xi_plus])
    alpha = np.column_stack([alpha_minus, alpha_plus])

    return SVMRegressionResult(
        beta0=beta0, beta=beta, xi=xi, margin=float(1.0 / (beta @ beta)), alpha=alpha
    )

svm_regression_primal(y, x, c, epsilon=None)

SVM regression, primal formulation.

Same c/epsilon semantics as svm_regression_dual except epsilon=0 is not a distinct primal branch in the original (only the LS-SVM epsilon<0 case and the epsilon>0 case are formulated directly).

Original: svm/svm_regression_primal.m

Source code in src/quanttoolbox/svm/svm.py
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
def svm_regression_primal(
    y: np.ndarray, x: np.ndarray, c: float, epsilon: float | None = None
) -> SVMRegressionResult:
    """SVM regression, primal formulation.

    Same c/epsilon semantics as ``svm_regression_dual`` except epsilon=0
    is not a distinct primal branch in the original (only the LS-SVM
    epsilon<0 case and the epsilon>0 case are formulated directly).

    Original: svm/svm_regression_primal.m
    """
    y = np.asarray(y, dtype=float).flatten()
    x = np.asarray(x, dtype=float)
    n, k = x.shape

    if epsilon is None or epsilon < 0:
        q = np.zeros((1 + k + n, 1 + k + n))
        q[1 : k + 1, 1 : k + 1] = np.eye(k)
        q[k + 1 :, k + 1 :] = 2 * c * np.eye(n)
        r = np.zeros(1 + k + n)
        a_eq = np.hstack([np.ones((n, 1)), x, np.eye(n)])
        b_eq = y

        theta = solve_qp(q, r, a_eq=a_eq, b_eq=b_eq)
        beta0, beta, xi = theta[0], theta[1 : k + 1], theta[k + 1 :]

    else:
        eps_vec = epsilon * np.ones(n)
        q = np.zeros((1 + k + 2 * n, 1 + k + 2 * n))
        q[1 : k + 1, 1 : k + 1] = np.eye(k)
        r = np.concatenate([np.zeros(1 + k), -c * np.ones(2 * n)])

        top = np.hstack([np.ones((n, 1)), x, -np.eye(n), np.zeros((n, n))])
        bottom = np.hstack([-np.ones((n, 1)), -x, np.zeros((n, n)), -np.eye(n)])
        c_ineq = np.vstack([top, bottom])
        d_ineq = np.concatenate([y + eps_vec, -y + eps_vec])
        lb = np.concatenate([np.full(1 + k, -1e10), np.zeros(2 * n)])

        theta = solve_qp(q, r, c_ineq=c_ineq, d_ineq=d_ineq, lb=lb)
        beta0, beta = theta[0], theta[1 : k + 1]
        xi = np.column_stack([theta[k + 1 : k + 1 + n], theta[k + 1 + n :]])

    margin = float(1.0 / (beta @ beta))
    return SVMRegressionResult(beta0=float(beta0), beta=beta, xi=xi, margin=margin)

Examples

Hard- and soft-margin SVM classification, dual formulation — svm/svm5.py
"""Translated from Examples/svm/svm5.m -- hard margin dual on the first
15 points, and soft margin dual (C=0.05) on the full 17-point dataset.
(The original's parameter sweep over C=0..0.4 for plotting purposes is
not re-translated separately -- svm3.py/svm4.py already demonstrate the
same sweep pattern at 4 representative C values.)"""

import numpy as np

from quanttoolbox.svm.svm import svm_classification_dual

data = np.array(
    [
        [0.5, 2.5, 1],
        [2.7, 4.2, 1],
        [2.7, 2.0, 1],
        [1.7, 4.2, 1],
        [1.5, 0.7, 1],
        [2.3, 5.3, 1],
        [4.0, 6.9, 1],
        [6.4, 4.5, -1],
        [7.7, 2.2, -1],
        [8.8, 6.0, -1],
        [7.4, 6.5, -1],
        [6.5, 1.7, -1],
        [8.3, 1.3, -1],
        [6.0, 1.3, -1],
        [5.0, 0.5, -1],
    ]
)
y15, x15 = data[:, 2], data[:, 0:2]
r_hard = svm_classification_dual(y15, x15, c=None)
print("hard margin: beta0=", round(r_hard.beta0, 6), "beta=", np.round(r_hard.beta, 6))

data17 = np.vstack([data, [[6.0, 5.0, 1], [2.0, 2.0, -1]]])
y17, x17 = data17[:, 2], data17[:, 0:2]
r_soft = svm_classification_dual(y17, x17, c=0.05, loss="hinge")
print("soft margin (C=0.05): beta0=", round(r_soft.beta0, 6), "beta=", np.round(r_soft.beta, 6))
Hard-margin SVM classification, dual formulation — svm/svm2.py
"""Translated from Examples/svm/svm2.m -- hard margin SVM classification,
dual formulation (same 15-point dataset as svm1.m)."""

import numpy as np

from quanttoolbox.svm.svm import svm_classification_dual

data = np.array(
    [
        [0.5, 2.5, 1],
        [2.7, 4.2, 1],
        [2.7, 2.0, 1],
        [1.7, 4.2, 1],
        [1.5, 0.7, 1],
        [2.3, 5.3, 1],
        [4.0, 6.9, 1],
        [6.4, 4.5, -1],
        [7.7, 2.2, -1],
        [8.8, 6.0, -1],
        [7.4, 6.5, -1],
        [6.5, 1.7, -1],
        [8.3, 1.3, -1],
        [6.0, 1.3, -1],
        [5.0, 0.5, -1],
    ]
)
y, x = data[:, 2], data[:, 0:2]

r = svm_classification_dual(y, x, c=None)
print("beta0:", round(r.beta0, 8))
print("beta:", np.round(r.beta, 8))
print("margin:", round(r.margin, 8))
print("support vectors (0-indexed):", r.support_vectors)
print("alpha at support vectors:", np.round(r.alpha[r.support_vectors], 8))
OLS, LAD, quantile, and SVM regression compared — svm/svm6.py
"""Translated from Examples/svm/svm6.m -- comparison of OLS, LAD, and
SVM-regression (LS-SVM and epsilon-SVM, at two different C values)
estimates on the same 10-obs dataset used in ols1.m/robust1.m."""

import numpy as np

from quanttoolbox.econometrics.estimation import ols_estimation
from quanttoolbox.stats.regression.quantile import quantile_regression
from quanttoolbox.stats.regression.robust import lad_regression
from quanttoolbox.svm.svm import svm_regression_primal

data = np.array(
    [
        [1.5, 1.0, 2.4, 3.6, 0.3],
        [20.4, 1.0, 1.1, 3.8, 5.9],
        [17.1, 1.0, 5.1, 6.3, 6.1],
        [30.9, 1.0, 2.7, 2.4, 9.5],
        [22.2, 1.0, 3.3, 3.0, 7.4],
        [9.1, 1.0, 1.0, 5.4, 4.9],
        [39.2, 1.0, 9.6, 2.8, 8.1],
        [3.1, 1.0, 2.9, 4.4, 1.0],
        [7.2, 1.0, 4.2, 5.6, 1.7],
        [27.6, 1.0, 8.1, 1.7, 5.4],
    ]
)
y = data[:, 0]
x_full = data[:, 1:5]

beta_ols = ols_estimation(y, x_full).beta
beta_lad = lad_regression(y, x_full).beta
beta_lad2, _, _ = quantile_regression(y, x_full, tau=0.5)

x = x_full[:, 1:4]  # drop the intercept and 4th column for SVM (3 predictors)

r_ls1 = svm_regression_primal(y, x, c=1)
r_eps1 = svm_regression_primal(y, x, c=1, epsilon=1)
r_ls2 = svm_regression_primal(y, x, c=100000)
r_eps2 = svm_regression_primal(y, x, c=100000, epsilon=0)

print("OLS:", np.round(beta_ols, 3))
print("LAD:", np.round(beta_lad, 3))
print("Quantile(0.5):", np.round(beta_lad2, 3))
print("SVM LS (C=1):", round(r_ls1.beta0, 3), np.round(r_ls1.beta, 3))
print("SVM eps (C=1, eps=1):", round(r_eps1.beta0, 3), np.round(r_eps1.beta, 3))
print("SVM LS (C=1e5):", round(r_ls2.beta0, 3), np.round(r_ls2.beta, 3))
print("SVM eps (C=1e5, eps=0):", round(r_eps2.beta0, 3), np.round(r_eps2.beta, 3))
OLS/SVM-LS and quantile/SVM-epsilon regression on synthetic data — svm/svm8.py
"""Translated from Examples/svm/svm8.m -- OLS, LAD, quantile, and SVM
regression (primal, at two very different C values, LS- and
epsilon-insensitive) compared on a larger (n=1000) simulated dataset.

The original draws x/beta/u from MATLAB's unseeded `rand`/`randn`; a fixed
seed (`np.random.default_rng(0)`) is substituted here for reproducibility
-- this is the fixed-seed substitution the tracker notes svm8.m as still
needing."""

import numpy as np

from quanttoolbox.econometrics.estimation import ols_estimation
from quanttoolbox.stats.regression.quantile import quantile_regression
from quanttoolbox.stats.regression.robust import lad_regression
from quanttoolbox.svm.svm import svm_regression_primal

rng = np.random.default_rng(0)
n, k = 1000, 4
x = rng.random((n, k))
beta_true = 5 * rng.random(k)
u = 0.20 * rng.standard_normal(n)
beta0_true = -3.0
y = beta0_true + x @ beta_true + u

x_design = np.column_stack([np.ones(n), x])

beta_ols = ols_estimation(y, x_design).beta
beta_lad = lad_regression(y, x_design).beta
beta_lad2, _, _ = quantile_regression(y, x_design, tau=0.5)

# SVM regression (primal), C=1
r_ls1 = svm_regression_primal(y, x, c=1)  # LS-SVM
beta_svm_ls = np.concatenate([[r_ls1.beta0], r_ls1.beta])
r_eps1 = svm_regression_primal(y, x, c=1, epsilon=1)  # epsilon-SVM
beta_svm_epsilon = np.concatenate([[r_eps1.beta0], r_eps1.beta])

# SVM regression (primal), C=1000 (effectively unregularized)
r_ls2 = svm_regression_primal(y, x, c=1000)  # LS-SVM
beta_svm_ls2 = np.concatenate([[r_ls2.beta0], r_ls2.beta])
r_eps2 = svm_regression_primal(y, x, c=1000, epsilon=0)  # epsilon-SVM, epsilon=0
beta_svm_epsilon2 = np.concatenate([[r_eps2.beta0], r_eps2.beta])

results = np.column_stack(
    [beta_ols, beta_lad, beta_lad2, beta_svm_ls, beta_svm_epsilon, beta_svm_ls2, beta_svm_epsilon2]
)
print("Comparison of OLS, LAD, quantile(0.5), and SVM estimates")
print(
    "columns: OLS, LAD, Quantile(0.5), SVM-LS(C=1), SVM-eps(C=1,eps=1), SVM-LS(C=1000), SVM-eps(C=1000,eps=0)"
)
print(np.round(results, 3))

print("\nOLS vs. SVM-LS at large C (expect near-identical -- unregularized limit):")
print(np.round(np.column_stack([beta_ols, beta_svm_ls2]), 3))

print(
    "\nQuantile(0.5) vs. SVM-eps at large C, epsilon=0 (expect near-identical -- both approach LAD):"
)
print(np.round(np.column_stack([beta_lad2, beta_svm_epsilon2]), 3))
Soft-margin SVM classification, dual formulation — svm/svm4.py
"""Translated from Examples/svm/svm4.m -- soft margin SVM classification
(binary hinge loss), dual formulation, at 4 different C values. Same
17-point dataset as svm3.m."""

import numpy as np

from quanttoolbox.svm.svm import svm_classification_dual

data = np.array(
    [
        [0.5, 2.5, 1],
        [2.7, 4.2, 1],
        [2.7, 2.0, 1],
        [1.7, 4.2, 1],
        [1.5, 0.7, 1],
        [2.3, 5.3, 1],
        [4.0, 6.9, 1],
        [6.4, 4.5, -1],
        [7.7, 2.2, -1],
        [8.8, 6.0, -1],
        [7.4, 6.5, -1],
        [6.5, 1.7, -1],
        [8.3, 1.3, -1],
        [6.0, 1.3, -1],
        [5.0, 0.5, -1],
        [6.0, 5.0, 1],
        [2.0, 2.0, -1],
    ]
)
y, x = data[:, 2], data[:, 0:2]

for c in [0.01, 0.03, 0.05, 0.30]:
    r = svm_classification_dual(y, x, c=c, loss="hinge")
    print(f"C={c}: beta0={round(r.beta0,6)} beta={np.round(r.beta,6)} margin={round(r.margin,6)}")
    print(f"  support vectors: {r.support_vectors}")
Soft-margin SVM classification, primal formulation — svm/svm3.py
"""Translated from Examples/svm/svm3.m -- soft margin SVM classification
(binary hinge loss), primal formulation, at 4 different C values. 17-point
dataset (2 extra, harder-to-separate points added vs. svm1/svm2)."""

import numpy as np

from quanttoolbox.svm.svm import svm_classification_primal

data = np.array(
    [
        [0.5, 2.5, 1],
        [2.7, 4.2, 1],
        [2.7, 2.0, 1],
        [1.7, 4.2, 1],
        [1.5, 0.7, 1],
        [2.3, 5.3, 1],
        [4.0, 6.9, 1],
        [6.4, 4.5, -1],
        [7.7, 2.2, -1],
        [8.8, 6.0, -1],
        [7.4, 6.5, -1],
        [6.5, 1.7, -1],
        [8.3, 1.3, -1],
        [6.0, 1.3, -1],
        [5.0, 0.5, -1],
        [6.0, 5.0, 1],
        [2.0, 2.0, -1],
    ]
)
y, x = data[:, 2], data[:, 0:2]

for c in [0.01, 0.03, 0.05, 0.30]:
    r = svm_classification_primal(y, x, c=c, loss="hinge")
    print(f"C={c}: beta0={round(r.beta0,6)} beta={np.round(r.beta,6)} margin={round(r.margin,6)}")
SVM regression, dual formulation, vs. the primal result — svm/svm7.py
"""Translated from Examples/svm/svm7.m -- same comparison as svm6.m
(OLS/LAD/quantile vs SVM regression), but using the dual formulation
(svm_regression_dual) instead of primal."""

import numpy as np

from quanttoolbox.svm.svm import svm_regression_dual

data = np.array(
    [
        [1.5, 1.0, 2.4, 3.6, 0.3],
        [20.4, 1.0, 1.1, 3.8, 5.9],
        [17.1, 1.0, 5.1, 6.3, 6.1],
        [30.9, 1.0, 2.7, 2.4, 9.5],
        [22.2, 1.0, 3.3, 3.0, 7.4],
        [9.1, 1.0, 1.0, 5.4, 4.9],
        [39.2, 1.0, 9.6, 2.8, 8.1],
        [3.1, 1.0, 2.9, 4.4, 1.0],
        [7.2, 1.0, 4.2, 5.6, 1.7],
        [27.6, 1.0, 8.1, 1.7, 5.4],
    ]
)
y = data[:, 0]
x_full = data[:, 1:5]
x = x_full[:, 1:4]

r_ls1 = svm_regression_dual(y, x, c=1)
r_eps1 = svm_regression_dual(y, x, c=1, epsilon=1)
r_ls2 = svm_regression_dual(y, x, c=100000)
r_eps2 = svm_regression_dual(y, x, c=100000, epsilon=0)

print("SVM dual LS (C=1):", round(r_ls1.beta0, 3), np.round(r_ls1.beta, 3))
print("SVM dual eps (C=1, eps=1):", round(r_eps1.beta0, 3), np.round(r_eps1.beta, 3))
print("SVM dual LS (C=1e5):", round(r_ls2.beta0, 3), np.round(r_ls2.beta, 3))
print("SVM dual eps (C=1e5, eps=0):", round(r_eps2.beta0, 3), np.round(r_eps2.beta, 3))
# expect these to match svm6.py's primal results exactly (strong duality)