{"id":"l-ml-2-3","courseId":"ml-crash-course","lang":"en","kind":"lesson","summary":"Add polynomial features to make a linear model bend, watch the training score rise while the test score collapses, and use a validation split to pick model complexity honestly.","html":"<p>A straight line is a strong assumption. If disease progression rises faster at high BMI than at low BMI, no straight line can capture that, however long gradient descent runs. The fix sounds harmless: let the model bend. This lesson shows how, and how quickly bending turns into memorising.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Bending a linear model</h2>\n<p>The trick is to keep linear regression and change the inputs. If you add a column <code dir=\"ltr\">bmi²</code>, a “linear” model in <code dir=\"ltr\">bmi</code> and <code dir=\"ltr\">bmi²</code> traces a parabola in <code dir=\"ltr\">bmi</code>. <code dir=\"ltr\">PolynomialFeatures</code> generates these columns: every power up to the chosen degree, plus every product of features (the <strong>interactions</strong>).</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"0\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.datasets </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> load_diabetes</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.preprocessing </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> PolynomialFeatures</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X, y </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> load_diabetes(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">return_X_y</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">True</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">as_frame</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">True</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">poly </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> PolynomialFeatures(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">degree</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">include_bias</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">False</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).fit(X[[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"bmi\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"bp\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]])</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(poly.get_feature_names_out())</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> degree </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    n </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> PolynomialFeatures(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">degree</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">degree, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">include_bias</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">False</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).fit(X).n_output_features_</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"degree </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">degree</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">n</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> columns from 10 features\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>Two features become five at degree 2. Ten features become 65 at degree 2 and 285 at degree 3. <code dir=\"ltr\">include_bias=False</code> drops the constant column, because <code dir=\"ltr\">LinearRegression</code> already fits an intercept.</p>\n<p>To chain the expansion and the regression, use <code dir=\"ltr\">make_pipeline</code>. It creates one estimator that runs each step in order; <code dir=\"ltr\">fit</code> fits each step on the training data and <code dir=\"ltr\">predict</code> sends new data through the same steps. You’ll build richer pipelines in section 4.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">The training score keeps climbing</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"1\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.datasets </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> load_diabetes</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.linear_model </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> LinearRegression</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.metrics </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> mean_absolute_error</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.model_selection </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> train_test_split</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.pipeline </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.preprocessing </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> PolynomialFeatures</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X, y </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> load_diabetes(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">return_X_y</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">True</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">as_frame</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">True</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X_train, X_test, y_train, y_test </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> train_test_split(X, y, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">test_size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.25</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">random_state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> degree </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    model </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline(PolynomialFeatures(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">degree</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">degree, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">include_bias</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">False</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">                          LinearRegression()).fit(X_train, y_train)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"degree </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">degree</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">: train R2 </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">model.score(X_train, y_train)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:6.3f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  \"</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">          f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"test R2 </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">model.score(X_test, y_test)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:7.3f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  \"</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">          f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"test MAE </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">mean_absolute_error(y_test, model.predict(X_test))</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:6.1f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p>Read the three rows slowly. Degree 2 improves the training score and worsens the test score. Degree 3 is a disaster dressed as a success: training R² of 0.90, test R² of -8.5, and a typical test miss of 151 points, worse than predicting the average for everyone. With 285 columns and only 331 training patients, the model has nearly enough knobs to pass through every training point. It has learned the noise.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Underfitting and overfitting</h2>\n<p>Those three rows map to a picture you’ll see in every ML course, because it’s the central trade-off of the field.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 220\" role=\"img\" aria-labelledby=\"l-ml-2-3-t1\"><defs><marker id=\"arrow-l-ml-2-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-ml-2-3-t1\">Three panels of the same curved scatter. Left: a straight line misses the curve (underfit). Middle: a gentle curve follows the trend (good fit). Right: a wiggly line passes through every point (overfit).</title>\n  <rect class=\"d-box\" x=\"10\" y=\"10\" width=\"220\" height=\"170\" rx=\"10\"/>\n  <rect class=\"d-box-success\" x=\"240\" y=\"10\" width=\"220\" height=\"170\" rx=\"10\"/>\n  <rect class=\"d-box\" x=\"470\" y=\"10\" width=\"220\" height=\"170\" rx=\"10\"/>\n  <circle class=\"d-dot\" cx=\"40\" cy=\"150\" r=\"4\"/><circle class=\"d-dot\" cx=\"70\" cy=\"118\" r=\"4\"/><circle class=\"d-dot\" cx=\"100\" cy=\"112\" r=\"4\"/><circle class=\"d-dot\" cx=\"130\" cy=\"88\" r=\"4\"/><circle class=\"d-dot\" cx=\"160\" cy=\"86\" r=\"4\"/><circle class=\"d-dot\" cx=\"190\" cy=\"60\" r=\"4\"/>\n  <path class=\"d-arrow\" d=\"M30 140 L210 70\"/>\n  <circle class=\"d-dot\" cx=\"270\" cy=\"150\" r=\"4\"/><circle class=\"d-dot\" cx=\"300\" cy=\"118\" r=\"4\"/><circle class=\"d-dot\" cx=\"330\" cy=\"112\" r=\"4\"/><circle class=\"d-dot\" cx=\"360\" cy=\"88\" r=\"4\"/><circle class=\"d-dot\" cx=\"390\" cy=\"86\" r=\"4\"/><circle class=\"d-dot\" cx=\"420\" cy=\"60\" r=\"4\"/>\n  <path class=\"d-arrow\" d=\"M260 160 Q330 90 440 50\" fill=\"none\"/>\n  <circle class=\"d-dot\" cx=\"500\" cy=\"150\" r=\"4\"/><circle class=\"d-dot\" cx=\"530\" cy=\"118\" r=\"4\"/><circle class=\"d-dot\" cx=\"560\" cy=\"112\" r=\"4\"/><circle class=\"d-dot\" cx=\"590\" cy=\"88\" r=\"4\"/><circle class=\"d-dot\" cx=\"620\" cy=\"86\" r=\"4\"/><circle class=\"d-dot\" cx=\"650\" cy=\"60\" r=\"4\"/>\n  <path class=\"d-arrow\" d=\"M490 165 C495 140 500 150 500 150 C515 100 525 125 530 118 C545 95 550 125 560 112 C575 60 580 95 590 88 C605 110 610 80 620 86 C635 95 640 40 650 60 C660 75 665 40 670 30\" fill=\"none\"/>\n  <text class=\"d-label-strong\" x=\"120\" y=\"205\" text-anchor=\"middle\">Underfit</text>\n  <text class=\"d-label-strong\" x=\"350\" y=\"205\" text-anchor=\"middle\">Good fit</text>\n  <text class=\"d-label-strong\" x=\"580\" y=\"205\" text-anchor=\"middle\">Overfit</text>\n</svg><figcaption>Underfitting, a good fit, and overfitting</figcaption></figure>\n<ul>\n<li><strong>Underfitting</strong> (high bias): the model is too simple for the pattern. Training and test scores are both poor and close together. More flexibility helps.</li>\n<li><strong>Overfitting</strong> (high variance): the model is flexible enough to chase noise. Training score is high, test score much lower. Less flexibility, more data or regularisation helps.</li>\n</ul>\n<p>You diagnose which side you’re on by comparing the two scores, never by looking at the training score alone. A small gap with poor scores says “add capacity”; a big gap says “take it away”.</p>\n<p>The amount of data shifts the balance. With 331 patients, 285 columns is reckless; with 300,000 patients, the same degree-3 model would have far less room to memorise, because noise in individual rows averages out across so many examples. That’s why “collect more data” is a legitimate cure for overfitting and a useless one for underfitting: a model that can’t express the pattern doesn’t get better at it by seeing more of it. When you’re unsure which side you’re on, a learning curve (training and validation scores plotted against the number of training rows) settles it: two curves that meet low need a richer model, two that stay apart need more data or a simpler model.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Choosing complexity with the test set</span></p>\n<p>It’s tempting to loop over degrees 1 to 6 and keep whichever scores best on the test set. That turns the test set into a tuning tool, and the score you report is no longer an honest estimate. Carve a <strong>validation</strong> set out of the training data, choose the degree there, then refit on all the training data and touch the test set once.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Choosing the degree honestly</h2>\n<p>Here’s that recipe with three features. The validation split comes from <code dir=\"ltr\">X_train</code>; the test set isn’t touched until the very last line.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"2\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.datasets </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> load_diabetes</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.linear_model </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> LinearRegression</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.model_selection </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> train_test_split</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.pipeline </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.preprocessing </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> PolynomialFeatures</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X, y </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> load_diabetes(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">return_X_y</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">True</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">as_frame</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">True</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> X[[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"bmi\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"bp\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"s5\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X_train, X_test, y_train, y_test </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> train_test_split(X, y, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">test_size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.25</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">random_state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X_fit, X_val, y_fit, y_val </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> train_test_split(X_train, y_train, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">test_size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.25</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">random_state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">def</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> poly_model</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(degree):</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline(PolynomialFeatures(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">degree</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">degree, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">include_bias</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">False</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), LinearRegression())</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">val_scores </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {d: poly_model(d).fit(X_fit, y_fit).score(X_val, y_val) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> d </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> range</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">7</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)}</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> d, s </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> val_scores.items():</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"degree </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">d</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">: validation R2 </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">s</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:6.3f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">best </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> max</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(val_scores, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">key</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">val_scores.get)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">final </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> poly_model(best).fit(X_train, y_train)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"chosen degree\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, best, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"| test R2\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">round</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(final.score(X_test, y_test), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span></code></pre><div class=\"run-output\" data-run-output=\"2\" hidden></div></div>\n<p>Validation picks degree 1, the straight line. On this data the curve isn’t worth its extra variance. That’s a common and useful outcome: the honest procedure often prefers the simple model that a peek at the test set would have talked you out of.</p>\n<p>One validation split of about 80 patients is itself noisy; a different <code dir=\"ltr\">random_state</code> could favour degree 2. Section 4 replaces it with cross-validation, which averages over several splits. The principle stays the same.</p>\n<p>Lowering the degree is a blunt way to control complexity: you either have the <code dir=\"ltr\">bmi × bp</code> column or you don’t. Next lesson keeps all the columns and instead penalises the model for using them heavily, which is a finer dial.</p>\n","toc":[{"id":"sec-1","text":"Bending a linear model","level":2},{"id":"sec-2","text":"The training score keeps climbing","level":2},{"id":"sec-3","text":"Underfitting and overfitting","level":2},{"id":"sec-4","text":"Choosing the degree honestly","level":2}],"takeaways":["<code dir=\"ltr\">PolynomialFeatures</code> adds squared and interaction columns, so a linear model can fit curves; the number of columns grows very fast with degree.","Underfitting means the model is too simple to capture the pattern; both training and test scores are poor.","Overfitting means the model has learned noise in the training rows; the training score is high and the test score much lower.","Choose complexity (degree, depth, number of features) on a validation split carved from the training data, never on the test set.","The usual cures for overfitting are a simpler model, more data, fewer features, or regularisation."],"further":[{"title":"PolynomialFeatures","url":"https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.PolynomialFeatures.html"},{"title":"Underfitting vs. Overfitting","url":"https://scikit-learn.org/stable/auto_examples/model_selection/plot_underfitting_overfitting.html"},{"title":"Validation curves","url":"https://scikit-learn.org/stable/modules/learning_curve.html"}],"quiz":[{"q":"<p>A model scores R² 0.52 on training data and 0.49 on test data. A more complex version scores 0.90 and -8.5. Which diagnosis fits?</p>","options":[{"text":"The simple model underfits and the complex one is better because it learned more.","why":"The complex model’s test score is far below zero, so whatever it learned doesn’t transfer. A small train/test gap is a healthy sign, not underfitting."},{"text":"The complex model overfits; it memorised the training rows and fails on new ones.","why":"Correct. A huge jump in training score paired with a collapse in test score is the signature of overfitting."},{"text":"Both models underfit because neither reaches 0.9 on the test set.","why":"Many real problems top out well below 0.9. Underfitting is judged by the training score being poor too, which isn’t the case for the complex model."}],"answer":1},{"q":"<p><code dir=\"ltr\">PolynomialFeatures(degree=2, include_bias=False)</code> on two columns <code dir=\"ltr\">a</code> and <code dir=\"ltr\">b</code> produces which columns?</p>","options":[{"text":"<code dir=\"ltr\">a</code>, <code dir=\"ltr\">b</code>, <code dir=\"ltr\">a^2</code>, <code dir=\"ltr\">a b</code>, <code dir=\"ltr\">b^2</code>","why":"Correct. Degree 2 adds every square and every pairwise product, the interaction term."},{"text":"<code dir=\"ltr\">a^2</code>, <code dir=\"ltr\">b^2</code>","why":"The original columns are kept, and the interaction <code dir=\"ltr\">a b</code> is included too."},{"text":"<code dir=\"ltr\">1</code>, <code dir=\"ltr\">a</code>, <code dir=\"ltr\">b</code>, <code dir=\"ltr\">a^2</code>, <code dir=\"ltr\">b^2</code>","why":"<code dir=\"ltr\">include_bias=False</code> drops the constant column, and the <code dir=\"ltr\">a b</code> interaction is missing from this list."}],"answer":0},{"q":"<p>How should you choose the polynomial degree?</p>","options":[{"text":"Pick the degree with the highest training score.","why":"The training score keeps rising with degree, so this always picks the most overfit model."},{"text":"Pick the degree with the highest test score, then report that score.","why":"That uses the test set to make a decision, so the reported score is optimistic."},{"text":"Split a validation set off the training data, pick the degree that scores best there, then measure once on the test set.","why":"Correct. The validation set absorbs the selection; the test set stays an unbiased final check."}],"answer":2},{"q":"<p>Which change is least likely to reduce overfitting?</p>","options":[{"text":"Collecting more training rows.","why":"More data makes it harder to memorise noise, so it usually helps."},{"text":"Adding more polynomial degrees so the model can fit the data more precisely.","why":"Correct. Extra flexibility is what causes overfitting in the first place."},{"text":"Lowering the degree or dropping weak features.","why":"A simpler model has less capacity to memorise, which is a direct cure."},{"text":"Adding a penalty on large coefficients.","why":"That’s regularisation, the subject of the next lesson, and a standard cure."}],"answer":1}],"exercise":{"kind":"python","starter":"from sklearn.datasets import load_diabetes\nfrom sklearn.linear_model import LinearRegression\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.preprocessing import PolynomialFeatures\n\nX, y = load_diabetes(return_X_y=True, as_frame=True)\nX = X[[\"bmi\", \"bp\", \"s5\", \"s3\"]]\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)\nX_fit, X_val, y_fit, y_val = train_test_split(X_train, y_train, test_size=0.25, random_state=0)\n\n# 1. val_scores: {degree: validation R2} for degrees 1 to 6.\n#    Fit each model on X_fit / y_fit, score it on X_val / y_val.\nval_scores = {}\n\n# 2. The degree with the best validation score\nbest_degree = None\n\n# 3. Refit that degree on ALL the training data, then score once on the test set\ntest_r2 = None\n","solution":"from sklearn.datasets import load_diabetes\nfrom sklearn.linear_model import LinearRegression\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.preprocessing import PolynomialFeatures\n\nX, y = load_diabetes(return_X_y=True, as_frame=True)\nX = X[[\"bmi\", \"bp\", \"s5\", \"s3\"]]\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)\nX_fit, X_val, y_fit, y_val = train_test_split(X_train, y_train, test_size=0.25, random_state=0)\n\ndef poly_model(degree):\n    return make_pipeline(PolynomialFeatures(degree=degree, include_bias=False), LinearRegression())\n\nval_scores = {d: poly_model(d).fit(X_fit, y_fit).score(X_val, y_val) for d in range(1, 7)}\nbest_degree = max(val_scores, key=val_scores.get)\ntest_r2 = poly_model(best_degree).fit(X_train, y_train).score(X_test, y_test)\nprint(val_scores, best_degree, test_r2)\n","checks":[{"id":"degrees","test":"sorted(val_scores) == [1, 2, 3, 4, 5, 6]"},{"id":"validation","test":"abs(float(val_scores[2]) - __import__('sklearn.pipeline', fromlist=['make_pipeline']).make_pipeline(__import__('sklearn.preprocessing', fromlist=['PolynomialFeatures']).PolynomialFeatures(degree=2, include_bias=False), __import__('sklearn.linear_model', fromlist=['LinearRegression']).LinearRegression()).fit(X_fit, y_fit).score(X_val, y_val)) < 1e-6"},{"id":"best","test":"best_degree == max(val_scores, key=val_scores.get)"},{"id":"test-score","test":"test_r2 is not None and abs(float(test_r2) - __import__('sklearn.pipeline', fromlist=['make_pipeline']).make_pipeline(__import__('sklearn.preprocessing', fromlist=['PolynomialFeatures']).PolynomialFeatures(degree=best_degree, include_bias=False), __import__('sklearn.linear_model', fromlist=['LinearRegression']).LinearRegression()).fit(X_train, y_train).score(X_test, y_test)) < 1e-6"}],"title":"Pick a polynomial degree without touching the test set","prompt":"<p>Using four diabetes features, choose the polynomial degree honestly.</p>\n<ol>\n<li><code dir=\"ltr\">val_scores</code>: a dict mapping each degree from 1 to 6 to its validation R². Each model is <code dir=\"ltr\">PolynomialFeatures(degree=d, include_bias=False)</code> followed by <code dir=\"ltr\">LinearRegression</code>, fitted on <code dir=\"ltr\">X_fit, y_fit</code> and scored on <code dir=\"ltr\">X_val, y_val</code>.</li>\n<li><code dir=\"ltr\">best_degree</code>: the degree with the highest validation R².</li>\n<li><code dir=\"ltr\">test_r2</code>: refit that degree on all of <code dir=\"ltr\">X_train, y_train</code> and score it once on <code dir=\"ltr\">X_test, y_test</code>.</li>\n</ol>\n","hints":["A helper <code dir=\"ltr\">poly_model(degree)</code> that returns <code dir=\"ltr\">make_pipeline(PolynomialFeatures(...), LinearRegression())</code> keeps the loop short.","A dict comprehension works: <code dir=\"ltr\">{d: poly_model(d).fit(X_fit, y_fit).score(X_val, y_val) for d in range(1, 7)}</code>.","<code dir=\"ltr\">max(val_scores, key=val_scores.get)</code> returns the key with the largest value."],"explanation":"<p>Validation scores fall away as the degree grows, and some higher degrees go negative, so the procedure settles on a low degree. Refitting on all of <code dir=\"ltr\">X_train</code> uses every training row for the final model, and the single test score is an unbiased estimate because the test set played no part in the choice.</p>\n","checkLabels":{"degrees":"val_scores has degrees 1 to 6","validation":"Validation scores come from models fitted on X_fit and scored on X_val","best":"best_degree has the highest validation score","test-score":"test_r2 comes from the chosen degree refitted on all training data"}},"words":706,"readMinutes":3}