Ridge, Lasso, and Elastic Net
After this lesson, you will:
- Understand regularization as augmenting the loss function with a penalty term against large weights, and explain why this tames overfitting in high-capacity models.
- Derive the closed-form analytical solution for Ridge regression directly from the regularized normal equation.
- Explain why Lasso forces coefficients to become exactly 0 while Ridge only shrinks them, through both single-variable calculus and geometric level curves.
- Implement Ridge, Lasso, and Elastic Net pipelines in scikit-learn with feature standardization, convert theoretical to package
alphaparameters, and select optimal regularization strengths via cross-validation.
Prerequisites
Section titled “Prerequisites”- Overfitting and Underfitting, in Machine Learning (Learned)
Intuition
Section titled “Intuition”Recall Carol from our previous lesson: her degree-15 polynomial passed through all 16 training points, but oscillated wildly between observations. Our earlier fix was lowering the degree to 3. This lesson explores the promised alternative: keep degree 15, but restrain the curve from contorting arbitrarily.
The degree-15 curve achieved its violent contortions through coefficients scaling into the tens of billions, alternating in sign: one power term pulls the curve upward furiously, and an adjacent term pulls downward with nearly equal force. At the 16 training coordinates, these massive opposing forces cancel out almost perfectly; between training points, the slightest mismatch catapults the curve off the charts. Bob’s smooth degree-3 wave required no such extreme coefficients. In short: astronomical coefficients are the primary symptom of overfitting.
Imagine charging a progressive tax on every coefficient proportional to its magnitude. The learning algorithm must balance two competing objectives: fitting training points closely (low MSE) while keeping coefficient taxes low (small weights). Spending large coefficient budgets to chase a stray noise particle is no longer economically viable. Two practical questions remain:
- How heavy should the tax be? Too lenient, and the model still overfits. Too punitive, and coefficients are crushed toward zero, collapsing the curve into a flat horizontal line (underfitting). The penalty rate is hyperparameter .
- How should the tax be structured? Proportional to squared coefficients (Ridge) or absolute values (Lasso). Crucially, Lasso drives many coefficients to exactly zero, whereas Ridge does not.
We use the same 24 points from the previous lesson: 16 training points for fitting, 8 test points for evaluation.
View the 24-point dataset
| x | y |
|---|---|
| 0.03 | 0.15 |
| 0.12 | 1.06 |
| 0.15 | 0.84 |
| 0.23 | 0.96 |
| 0.26 | 0.83 |
| 0.35 | 0.94 |
| 0.41 | 0.30 |
| 0.47 | −0.09 |
| 0.51 | −0.17 |
| 0.60 | −0.32 |
| 0.64 | −0.68 |
| 0.73 | −0.84 |
| 0.77 | −0.28 |
| 0.85 | −0.82 |
| 0.90 | −0.53 |
| 0.98 | −0.02 |
| x | y |
|---|---|
| 0.06 | 0.33 |
| 0.18 | 0.66 |
| 0.32 | 0.93 |
| 0.43 | 0.43 |
| 0.55 | −0.42 |
| 0.67 | −1.11 |
| 0.80 | −1.18 |
| 0.94 | −0.45 |
The mission of this lesson is: retain the degree-15 polynomial, augment MSE with a penalty on large weights, and tune penalty strength empirically so the curve relaxes into a smooth function that accurately predicts new inputs.
Theory
Section titled “Theory”The penalized objective function
Section titled “The penalized objective function”The underlying model remains a degree-15 polynomial, structured as linear regression over engineered features: . The empirical loss remains MSE over training instances. Regularization replaces minimizing with minimizing a joint objective function , combining data fidelity with a complexity penalty:
- is the penalty strength hyperparameter. Setting recovers ordinary least squares (Carol’s interpolating curve).
- is the squared L2 norm (squared Euclidean length). is the L1 norm (Manhattan length). Hence, Ridge is L2 regularization, and Lasso is L1 regularization.
- The intercept is never penalized. The intercept merely translates the entire curve vertically without altering curvature. Penalizing artificially drags predictions toward .
Ridge was formulated by Hoerl and Kennard (1970). Lasso was introduced by Robert Tibshirani (1996), standing for “least absolute shrinkage and selection operator”: it simultaneously performs parameter shrinkage and feature selection.
Standardizing features before penalization
Section titled “Standardizing features before penalization”The penalty applies uniform weight to all , but the scale of inversely depends on the measurement unit of feature . If is converted from meters to centimeters, its values grow by 100, forcing to shrink by 100 to maintain identical predictions, which reduces penalty by 10 000. Changing arbitrary units alters the resulting mathematical fit.
The standard solution is feature standardization: , where and are the sample mean and standard deviation estimated on training data (StandardScaler). Standardized features have mean 0 and variance 1, ensuring equitable penalization. In polynomial models, , and corresponds to standardized term .
Ridge: closed-form solution
Section titled “Ridge: closed-form solution”Let denote the matrix where row , column holds . Unlike unpenalized regression, omits the intercept column of ones, because is solved independently. Because each column is zero-centered, column sums vanish: , where is the vector of ones. The Ridge objective is:
Step 1: solve for . The penalty excludes , so the partial derivative with respect to matches standard regression: . Setting this to zero and substituting :
Because every standardized column sums to zero, strictly equals the training sample mean , regardless of .
Step 2: substitute . Define centered labels . Then , isolating :
Step 3: compute gradient with respect to and set to zero. As in ordinary least squares, the gradient of the MSE term is . The gradient of is , assembling into vector :
Step 4: isolate parameter vector .
Multiply through by and expand. Factor , where is the identity matrix. Finally, , yielding:
Compared to the standard normal equation, this simply adds along the diagonal. This modest diagonal boost changes everything:
- Always invertible for . For any non-zero vector , , guaranteeing positive definiteness and unique invertibility, even under severe collinearity or when features outnumber samples ().
- Translating to code. Scikit-learn’s
Ridgeminimizes unscaled residual sum of squares , which equals when setting . Hence, our implementation setsRidge(alpha=m * lam).
Why coefficients shrink: the 1D case
Section titled “Why coefficients shrink: the 1D case”Consider a single standardized feature , satisfying . Let and . Expanding MSE in terms of :
Because is independent of , MSE forms a parabola centered at , which represents the unpenalized least-squares solution.
Under Ridge regularization, we minimize . Setting the derivative to zero:
Ridge divides by factor : the weight scales down smoothly while preserving its original sign, but never reaches absolute zero unless .
In multidimensional problems, Ridge shrinks most aggressively along directions where empirical data provides the least evidentiary support. Collinear polynomial features (such as and having correlation 0.9996) allow opposing weights to explode with negligible effect on MSE. Even a minuscule penalty squashes these unstable directions.
Why Lasso produces exact zeros
Section titled “Why Lasso produces exact zeros”Single-variable calculus. Under Lasso, we minimize . Because absolute value is non-differentiable at zero, we evaluate piecewise:
- : derivative is , vanishing at , valid strictly when .
- : derivative is , vanishing at , valid strictly when .
- When : the right derivative is strictly positive for , while the left derivative is strictly negative for . The objective increases in both directions away from zero, achieving its global minimum at .
Lasso subtracts a constant threshold offset from magnitude and clamps at zero—an operation called soft-thresholding. Comparing both methods for :
| Ridge: | Lasso | |
|---|---|---|
| 0.2 | 0.25 | 0.2 |
| 0.4 | 0.2143 | 0.1 |
| 1 | 0.15 | 0 |
The root difference lies in the slope of the penalty near the origin. The Ridge penalty has derivative , which flattens to zero as ; even the weakest empirical signal pulls away from zero. The Lasso penalty maintains a constant slope of all the way to the origin: data evidence () must overcome threshold for to escape zero.
Geometric intuition. It can be shown that each maps to a constraint budget framing an equivalent problem: minimize MSE subject to weight budget . In 2D parameter space, the Ridge constraint region is a smooth circle; the Lasso constraint is a rotated square (diamond) with sharp vertices anchored on coordinate axes. Contours of MSE form concentric ellipses centered at the unconstrained minimum. Expanding ellipses contact the constraint boundary at the constrained optimum.
Because circles lack protruding corners, contact typically occurs along smooth arcs where both parameters remain non-zero. The diamond possesses sharp vertices along coordinate axes, making it highly probable for expanding ellipses to touch a vertex first, setting one weight exactly to zero. In 15 dimensions, the L1 cross-polytope has many vertices and low-dimensional facets, zeroing out numerous features simultaneously and producing sparse models.
Lasso lacks an analytical closed-form solution. Scikit-learn optimizes Lasso via coordinate descent: cycling through coefficients sequentially, applying 1D soft-thresholding at each update.
Elastic Net
Section titled “Elastic Net”Elastic Net (Zou and Hastie, 2005) combines both penalty structures:
Setting yields Lasso; setting yields Ridge. When features exhibit severe multicollinearity (such as and ), Lasso tends to pick one arbitrarily and zero out the other. The L2 component encourages correlated features to share weights jointly, while the L1 component preserves exact sparsity. In scikit-learn, hyperparameters map to alpha and l1_ratio (detailed in code cell 3).
Tuning λ
Section titled “Tuning λ”Penalty weight is a hyperparameter tuned identically to polynomial degree: scan candidate values across a geometric grid (typically powers of 10), identify the setting minimizing cross-validation MSE, re-fit the entire training set, and score once on the test set. Encapsulate StandardScaler inside the pipeline to ensure cross-validation folds compute normalization statistics strictly on their respective training partitions.
Limitations
Section titled “Limitations”- Cannot overcome poor model family choices. On this dataset, regularized degree-15 polynomials still fall short of an unpenalized degree-3 model (see benchmark table).
- Optimal shows variance on small sample sizes.
- Lasso feature selection is unstable under severe collinearity. Do not interpret retained features as absolute ground-truth indicators of physical importance.
- Broad applicability. Regularization generalizes beyond linear regression: logistic regression and deep neural networks routinely employ L2 weight decay.
Illustration
Section titled “Illustration”Explore regularization interactively. The plot displays 16 training samples (solid dots), 8 test samples (open circles), and the regularized degree-15 polynomial curve. The slider navigates 17 discrete penalty values: 0, followed by logarithmic steps from to . Toggling Ridge and Lasso compares penalty types at identical . The lower panel displays empirical MSE scores, exact zero counts, and individual coefficient bar magnitudes . Try:
- Initial state: Ridge at : interpolates all training dots, test MSE is 133.20, and all coefficient bars clip the chart.
- Advance to : train MSE is 0.0491, test MSE reaches 0.0894 (slider minimum, marked ✓), and all bars stay well within bounds.
- Switch to Lasso: train MSE is 0.0314, test MSE improves to 0.0851, and 10 of 15 coefficients become exactly zero, leaving only .
- Advance to : Ridge yields train MSE 0.3148, test MSE 0.4750 (underfitting). Lasso zeros out all 15 weights, flattening into a horizontal line at sample mean .
The checkmark highlights test set minima for pedagogical demonstration (which cannot be done in production). The Lasso trajectory is pre-calculated using high-precision optimization because standard coordinate descent converges sluggishly at tiny penalties.
- Training point
- Test point
- Fitted degree-15 polynomial
- Off the chart (absolute value above 10)
Overfitting: the penalty is too weak; the coefficients are huge, the curve follows the noise, and test MSE is high.
16 training points, x from 0.03 to 0.98, follow one wave: up near 1 for x from 0.1 to 0.35, down near −1 for x from 0.7 to 0.85, then back near 0. The points stray from the wave because of noise. 8 test points sit between the training points and follow the same wave.
Each bar is the coefficient wₖ of the standardized feature xᵏ, for k from 1 to 15. Bars up are positive, bars down are negative.
Code examples
Section titled “Code examples”The three cells below run independently. Each loads the 24-point dataset self-containedly. Pipelines consistently chain PolynomialFeatures to generate , StandardScaler to normalize terms, and regularized estimators. Access the final estimator via model[-1], exposing coef_ () and intercept_ ().
Ridge across penalty strengths
Section titled “Ridge across penalty strengths”The first cell evaluates Ridge across five orders of magnitude, reporting error metrics, maximum coefficient magnitude, and intercept .
Ridge on degree-15 polynomial
import numpy as npfrom sklearn.linear_model import Ridgefrom sklearn.metrics import mean_squared_errorfrom sklearn.pipeline import make_pipelinefrom sklearn.preprocessing import PolynomialFeatures, StandardScaler
# 24 points made from y = sin(2πx) plus noise: 16 training, 8 test points.# dataset: sine-trainx_train = np.array([0.03, 0.12, 0.15, 0.23, 0.26, 0.35, 0.41, 0.47, 0.51, 0.6, 0.64, 0.73, 0.77, 0.85, 0.9, 0.98])y_train = np.array([0.15, 1.06, 0.84, 0.96, 0.83, 0.94, 0.3, -0.09, -0.17, -0.32, -0.68, -0.84, -0.28, -0.82, -0.53, -0.02])# dataset: sine-testx_test = np.array([0.06, 0.18, 0.32, 0.43, 0.55, 0.67, 0.8, 0.94])y_test = np.array([0.33, 0.66, 0.93, 0.43, -0.42, -1.11, -1.18, -0.45])
m = len(x_train) # 16 training pointsfor lam in [1e-12, 1e-8, 1e-3, 1e-1, 10]: # scikit-learn's Ridge does not divide the sum of squares by m, # so set alpha = m * lambda to match the lesson's objective. model = make_pipeline(PolynomialFeatures(15, include_bias=False), StandardScaler(), Ridge(alpha=m * lam)) model.fit(x_train[:, None], y_train) # learn from the training set only tr = mean_squared_error(y_train, model.predict(x_train[:, None])) te = mean_squared_error(y_test, model.predict(x_test[:, None])) w = model[-1].coef_ # the 15 weights of the standardized features b = model[-1].intercept_ # the bias, not penalized print(f"lambda = {lam:g}: MSE train = {tr:.4f}, test = {te:.4f}, " f"max |w| = {np.abs(w).max():.2f}, b = {b:.4f}")The first run downloads Python, about 10 MB.
A minuscule penalty of slashes test MSE from ~133 down to 0.5375, bringing maximum weights down from billions to 26 594.49. At , test MSE achieves its minimum (0.0894) with maximum coefficient down to 2.75. At , training MSE surges to 0.3148—worse than a simple straight line (0.1663)—exhibiting severe underfitting. Intercept remains constant at (matching training mean ) across all runs, validating Step 1.
Lasso and sparse coefficients
Section titled “Lasso and sparse coefficients”The second cell evaluates Lasso across three penalty strengths, counting exact zeros (w == 0) and listing surviving features. Ridge at serves as a comparison baseline.
Lasso: counting zero coefficients
import numpy as npfrom sklearn.linear_model import Lasso, Ridgefrom sklearn.metrics import mean_squared_errorfrom sklearn.pipeline import make_pipelinefrom sklearn.preprocessing import PolynomialFeatures, StandardScaler
# 24 points made from y = sin(2πx) plus noise: 16 training, 8 test points.# dataset: sine-trainx_train = np.array([0.03, 0.12, 0.15, 0.23, 0.26, 0.35, 0.41, 0.47, 0.51, 0.6, 0.64, 0.73, 0.77, 0.85, 0.9, 0.98])y_train = np.array([0.15, 1.06, 0.84, 0.96, 0.83, 0.94, 0.3, -0.09, -0.17, -0.32, -0.68, -0.84, -0.28, -0.82, -0.53, -0.02])# dataset: sine-testx_test = np.array([0.06, 0.18, 0.32, 0.43, 0.55, 0.67, 0.8, 0.94])y_test = np.array([0.33, 0.66, 0.93, 0.43, -0.42, -1.11, -1.18, -0.45])
def fit(model): # Degree-15 polynomial, standardized features, then a penalized model. pipe = make_pipeline(PolynomialFeatures(15, include_bias=False), StandardScaler(), model) return pipe.fit(x_train[:, None], y_train)
for lam in [1e-3, 1e-2, 1e-1]: # scikit-learn's Lasso puts 1/2 in front of the MSE: alpha = lambda / 2. model = fit(Lasso(alpha=lam / 2, max_iter=100_000, tol=1e-6)) tr = mean_squared_error(y_train, model.predict(x_train[:, None])) te = mean_squared_error(y_test, model.predict(x_test[:, None])) w = model[-1].coef_ kept = np.flatnonzero(w) + 1 # the power k of each nonzero weight print(f"lambda = {lam:g}: MSE train = {tr:.4f}, test = {te:.4f}") print(f" zeros = {np.sum(w == 0)}/15, kept k = {kept.tolist()}")
# Compare: Ridge at the same lambda = 0.001 sets no weight exactly to 0.w = fit(Ridge(alpha=len(x_train) * 1e-3))[-1].coef_print(f"ridge 0.001: zeros = {np.sum(w == 0)}/15, " f"min |w| = {np.abs(w).min():.2f}")The first run downloads Python, about 10 MB.
- At , Lasso achieves test MSE 0.0851 (edging out Ridge’s 0.0894) while retaining only 5 active features: . At , active features drop to 4; at , only 3 remain.
- Ridge at identical strength has 0 zero coefficients; its smallest weight is 0.07, small but non-zero.
Active indices shift non-monotonically between adjacent runs (e.g. trading powers 4 and 9 for 6 and 7) due to high collinearity among normalized powers—the exact motivation behind Elastic Net. We avoid because coordinate descent struggles to converge at extreme limits.
Selecting λ via cross-validation
Section titled “Selecting λ via cross-validation”The third cell tunes exclusively on 16 training points using 4-fold cross-validation (KFold(n_splits=4, shuffle=True, random_state=0)). Ridge evaluates a logarithmic grid from to 10. Lasso and Elastic Net () evaluate 0.01 and 0.1 to avoid fold-level convergence warnings.
Selecting λ via cross-validation
import numpy as npfrom sklearn.linear_model import ElasticNet, Lasso, Ridgefrom sklearn.metrics import mean_squared_errorfrom sklearn.model_selection import KFold, cross_val_scorefrom sklearn.pipeline import make_pipelinefrom sklearn.preprocessing import PolynomialFeatures, StandardScaler
# dataset: sine-trainx_train = np.array([0.03, 0.12, 0.15, 0.23, 0.26, 0.35, 0.41, 0.47, 0.51, 0.6, 0.64, 0.73, 0.77, 0.85, 0.9, 0.98])y_train = np.array([0.15, 1.06, 0.84, 0.96, 0.83, 0.94, 0.3, -0.09, -0.17, -0.32, -0.68, -0.84, -0.28, -0.82, -0.53, -0.02])# dataset: sine-testx_test = np.array([0.06, 0.18, 0.32, 0.43, 0.55, 0.67, 0.8, 0.94])y_test = np.array([0.33, 0.66, 0.93, 0.43, -0.42, -1.11, -1.18, -0.45])
# Ridge keeps alpha = 16 * lambda even in 12-point folds (as RidgeCV does).makers = { "ridge": lambda lam: Ridge(alpha=len(x_train) * lam), "lasso": lambda lam: Lasso(alpha=lam / 2, max_iter=100_000, tol=1e-6), # lambda_1 = lambda_2 = lam: alpha = lam/2 + lam, l1_ratio = (lam/2)/alpha "enet": lambda lam: ElasticNet(alpha=1.5 * lam, l1_ratio=1 / 3, max_iter=100_000, tol=1e-6),}cv = KFold(n_splits=4, shuffle=True, random_state=0) # training set onlyfor name, make in makers.items(): # Lasso at 0.001 does not converge within the folds: start at 0.01. lams = 10.0 ** np.arange(-6, 2) if name == "ridge" else [0.01, 0.1] pipes, cv_mse = {}, {} for lam in lams: pipes[lam] = make_pipeline(PolynomialFeatures(15, include_bias=False), StandardScaler(), make(lam)) scores = cross_val_score(pipes[lam], x_train[:, None], y_train, cv=cv, scoring="neg_mean_squared_error") cv_mse[lam] = -scores.mean() print(f"{name} lambda = {lam:g}: CV MSE = {cv_mse[lam]:.4f}") best = min(cv_mse, key=cv_mse.get) # Only then refit on the whole training set and score the test set once. model = pipes[best].fit(x_train[:, None], y_train) te = mean_squared_error(y_test, model.predict(x_test[:, None])) print(f" best: lambda = {best:g}, test MSE = {te:.4f}, " f"zeros = {np.sum(model[-1].coef_ == 0)}")The first run downloads Python, about 10 MB.
Cross-validation selects Ridge (CV MSE 0.1605), producing an honest test MSE of 0.1747. It does not pick , which achieved post-hoc optimality on test points: under CV, scored 0.3409 (twice as high).
Why the difference? Test points happen to interleave neatly between training samples (). Cross-validation folds, by contrast, occasionally leave outer boundary points (like or ) unobserved, forcing candidate polynomials to extrapolate into empty margins. Lightly regularized high-degree curves swing violently at boundaries: when holding out , squared error at that point is 0.88 under but only 0.02 under . Cross-validation penalizes boundary volatility heavily, favoring stronger regularizers. The resulting 0.1747 test score is realistic and unbiased.
Lasso selects : test MSE is 0.1341 with 11 zero coefficients. Elastic Net selects : CV MSE is 0.1696, test MSE is 0.1481, and 6 coefficients become zero because L2 sharing prevents dropping collinear groups entirely.
Is regularization worthwhile?
Section titled “Is regularization worthwhile?”| Model | Selection protocol | Test MSE |
|---|---|---|
| Degree 15, unregularized | ~133 | |
| Degree 3, unregularized | Cross-validation (previous lesson) | 0.0754 |
| Ridge degree 15, | Test set inspection (invalid protocol) | 0.0894 |
| Lasso degree 15, | Test set inspection (invalid protocol) | 0.0851 |
| Ridge degree 15, | Cross-validation | 0.1747 |
| Lasso degree 15, | Cross-validation | 0.1341 |
| Elastic Net degree 15, | Cross-validation | 0.1481 |
Directly put: on this specific dataset, choosing the correct degree 3 remains superior. Even when unfairly cherry-picking optimal from test observations, Ridge lags degree 3 by ~19%, and Lasso lags by ~13%. Because the true generative process is structurally close to a cubic polynomial, degree tuning represents the exact natural knob for this problem. Why then is regularization invaluable?
- Eliminates combinatorial subset search. In 1D polynomial modeling, powers have a natural linear hierarchy (). With 50 unstructured real-world features (area, bedrooms, transit distance), there is no single “degree”, and exhaustive feature subset search requires evaluating (~) combinations. Regularization compresses all feature weights into a single continuous tuning knob.
- Prevents catastrophic divergence. Regularization tames errors from 133 down to 0.1747 via a single scalar penalty.
- Automates feature selection. Lasso zeroes out uninformative variables automatically, and joint cross-validation can tune capacity and regularization simultaneously.
Common pitfalls
Section titled “Common pitfalls”Quick check
Section titled “Quick check”Summary and further reading
Section titled “Summary and further reading”Key takeaways:
- Regularization augments MSE loss with complexity penalties: Ridge adds , Lasso adds , and Elastic Net incorporates both; intercept is never penalized. Overly weak penalties cause overfitting; excessive penalties trigger underfitting.
- Ridge regression yields and on standardized inputs. Coefficients shrink smoothly toward zero without reaching exact sparsity.
- Lasso applies soft-thresholding ( when ), producing sparse models. The L1 constraint diamond features sharp vertices anchored directly on coordinate axes.
- Always encapsulate feature standardization within cross-validation pipelines. Convert theoretical to package parameter
alphaaccurately (Ridge: , Lasso: ). Select penalty strengths via cross-validation, even when resulting models are more conservative than post-hoc test minima.
Further reading:
- Vũ Hữu Tiệp (2017). Machine Learning cơ bản, Lesson 15: Overfitting, regularization section. Accessible Vietnamese guide covering L2 weight decay, L1, and Elastic Net formulations.
- scikit-learn developers (2007–2026). scikit-learn 1.8 User Guide: Linear Models, sections on Ridge, Lasso, and Elastic-Net. Detailed mathematical objective definitions ensuring proper
alphaparameter mappings. - James, G., Witten, D., Hastie, T., Tibshirani, R. (2013). An Introduction to Statistical Learning (PDF), 1st ed., Chapter 6: Linear Model Selection and Regularization. In-depth conceptual analysis of shrinkage and geometry.
- Tibshirani, R. (1996). Regression Shrinkage and Selection via the Lasso. Journal of the Royal Statistical Society: Series B 58(1), 267–288. Foundational paper introducing the Lasso algorithm.
- Hoerl, A. E., Kennard, R. W. (1970). Ridge Regression: Biased Estimation for Nonorthogonal Problems. Technometrics 12(1), 55–67. And Zou, H., Hastie, T. (2005). Regularization and Variable Selection via the Elastic Net. Journal of the Royal Statistical Society: Series B 67(2), 301–320. Original milestone publications defining Ridge regression and Elastic Net.
Your browser is blocking storage. Your progress will be lost when you reload the page.
Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About