{"id":"l-ml-4-2","courseId":"ml-crash-course","lang":"en","kind":"lesson","summary":"Build one Pipeline that imputes, scales and one-hot encodes the right columns with ColumnTransformer, so preprocessing is refit inside every cross-validation fold and travels with the model.","html":"<p>Section 3 kept leaving data on the table. <code dir=\"ltr\">country</code> and <code dir=\"ltr\">segment</code> are text, which no scikit-learn model accepts directly. <code dir=\"ltr\">avg_satisfaction</code> has gaps that logistic regression refuses. And you’ve been scaling by hand, keeping track of <code dir=\"ltr\">X_train_s</code> and <code dir=\"ltr\">X_test_s</code>. Each of those steps has to be learned from training data only, applied identically to new data, and redone inside every cross-validation fold. Doing that by hand is how leaks and bugs happen. A pipeline does it for you.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Pipelines bundle steps into one estimator</h2>\n<p>A <code dir=\"ltr\">Pipeline</code> is a list of steps: any number of <strong>transformers</strong> (objects with <code dir=\"ltr\">fit</code> and <code dir=\"ltr\">transform</code>, such as <code dir=\"ltr\">StandardScaler</code>) followed by one final estimator. The pipeline behaves like a single model:</p>\n<ul>\n<li><code dir=\"ltr\">pipe.fit(X, y)</code> fits the first transformer, transforms the data, passes it to the next step, and so on, then fits the model on the result.</li>\n<li><code dir=\"ltr\">pipe.predict(X_new)</code> runs <code dir=\"ltr\">X_new</code> through the already-fitted transformers, then predicts.</li>\n</ul>\n<p>You’ve used <code dir=\"ltr\">make_pipeline</code>, which names steps automatically (<code dir=\"ltr\">&quot;standardscaler&quot;</code>, <code dir=\"ltr\">&quot;logisticregression&quot;</code>). <code dir=\"ltr\">Pipeline([(&quot;prep&quot;, ...), (&quot;model&quot;, ...)])</code> lets you choose the names, which you’ll want next lesson when you tune parameters by name.</p>\n<p>The real payoff is in cross-validation. <code dir=\"ltr\">cross_validate</code> clones the entire pipeline and refits it in every fold, so the scaler’s means, the imputer’s medians and the encoder’s category list are learned from that fold’s training rows only.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Different columns, different treatment</h2>\n<p>Not every column needs the same preprocessing. <code dir=\"ltr\">ColumnTransformer</code> routes each group of columns through its own transformer and glues the outputs side by side.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 280\" role=\"img\" aria-labelledby=\"l-ml-4-2-t1\"><defs><marker id=\"arrow-l-ml-4-2-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-4-2-t1\">The raw churn table splits into three column groups. Eight numeric columns go through StandardScaler. avg_satisfaction goes through SimpleImputer with an indicator, then StandardScaler. country and segment go through OneHotEncoder. The outputs are concatenated into 21 columns and fed to LogisticRegression.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"110\" width=\"120\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"70\" y=\"134\" text-anchor=\"middle\">raw table</text>\n  <text class=\"d-label-muted\" x=\"70\" y=\"154\" text-anchor=\"middle\">11 columns</text>\n  <path class=\"d-arrow\" d=\"M130 125 L190 50\" marker-end=\"url(#arrow-l-ml-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M130 138 L190 138\" marker-end=\"url(#arrow-l-ml-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M130 151 L190 226\" marker-end=\"url(#arrow-l-ml-4-2-1)\"/>\n  <rect class=\"d-box-accent\" x=\"195\" y=\"24\" width=\"250\" height=\"52\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"320\" y=\"46\" text-anchor=\"middle\">8 numeric</text>\n  <text class=\"d-label\" x=\"320\" y=\"66\" text-anchor=\"middle\">StandardScaler</text>\n  <rect class=\"d-box-warn\" x=\"195\" y=\"112\" width=\"250\" height=\"52\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"320\" y=\"134\" text-anchor=\"middle\">avg_satisfaction</text>\n  <text class=\"d-label\" x=\"320\" y=\"154\" text-anchor=\"middle\">SimpleImputer + indicator, scaler</text>\n  <rect class=\"d-box-success\" x=\"195\" y=\"200\" width=\"250\" height=\"52\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"320\" y=\"222\" text-anchor=\"middle\">country, segment</text>\n  <text class=\"d-label\" x=\"320\" y=\"242\" text-anchor=\"middle\">OneHotEncoder</text>\n  <path class=\"d-arrow\" d=\"M445 50 L505 125\" marker-end=\"url(#arrow-l-ml-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M445 138 L505 138\" marker-end=\"url(#arrow-l-ml-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M445 226 L505 151\" marker-end=\"url(#arrow-l-ml-4-2-1)\"/>\n  <rect class=\"d-box-primary\" x=\"510\" y=\"104\" width=\"180\" height=\"68\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"600\" y=\"130\" text-anchor=\"middle\">21 columns</text>\n  <text class=\"d-label\" x=\"600\" y=\"152\" text-anchor=\"middle\">LogisticRegression</text>\n</svg><figcaption>A ColumnTransformer inside a Pipeline</figcaption></figure>\n<p>Three transformers do the work:</p>\n<ul>\n<li><strong><code dir=\"ltr\">OneHotEncoder</code></strong> creates one 0/1 column per category: <code dir=\"ltr\">country_Egypt</code>, <code dir=\"ltr\">country_Germany</code> and so on. Set <code dir=\"ltr\">handle_unknown=&quot;ignore&quot;</code>; the default raises an error the first time a customer arrives from a country the training data didn’t include, which in production means a crashed prediction service.</li>\n<li><strong><code dir=\"ltr\">SimpleImputer</code></strong> fills missing values with the mean, median, most frequent value or a constant, learned from training data. With <code dir=\"ltr\">add_indicator=True</code> it also adds a column that is 1 where the value was missing. Half of Cartwheel’s customers never answered the survey, and that fact may matter more than the score itself.</li>\n<li><strong><code dir=\"ltr\">StandardScaler</code></strong>, as before. A small <code dir=\"ltr\">make_pipeline</code> lets one column group pass through two steps in order.</li>\n</ul>\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\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pandas </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.compose </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ColumnTransformer</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.impute </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> SimpleImputer</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\"> LogisticRegression</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\"> StratifiedKFold, cross_val_score, 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\"> Pipeline, 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\"> OneHotEncoder, StandardScaler</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">churn </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churn.csv\"</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\"> churn.drop(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">columns</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"customer_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churned\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">y </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> churn[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churned\"</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 style=\"color:#953800;--shiki-dark:#FFA657\">stratify</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">y)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">numeric </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tenure_days\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total_spent\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"avg_order_value\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">           \"days_since_last_order\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"used_coupon\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"support_tickets\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mobile_share\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">preprocess </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ColumnTransformer([</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    (</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"num\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, StandardScaler(), numeric),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    (</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"sat\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, make_pipeline(SimpleImputer(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">strategy</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"median\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">add_indicator</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\">), StandardScaler()),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">     [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"avg_satisfaction\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    (</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"cat\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, OneHotEncoder(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">handle_unknown</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"ignore\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"country\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"segment\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">pipe </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Pipeline([(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"prep\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, preprocess), (</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"model\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, LogisticRegression())])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">pipe.fit(X_train, y_train)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">names </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pipe.named_steps[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"prep\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].get_feature_names_out()</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:#0550AE;--shiki-dark:#79C0FF\">len</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(names), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"model inputs, e.g.\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">list</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(names[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">12</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">cv </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> StratifiedKFold(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n_splits</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">shuffle</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\">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:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"all columns  ROC AUC:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, cross_val_score(pipe, X_train, y_train, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">cv</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">cv, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">scoring</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"roc_auc\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).mean().round(</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\">numeric_only </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline(StandardScaler(), LogisticRegression())</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\">\"numeric only ROC AUC:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, cross_val_score(numeric_only, X_train[numeric], y_train, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">cv</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">cv, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">scoring</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"roc_auc\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).mean().round(</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=\"0\" hidden></div></div>\n<p>The pipeline accepts the raw DataFrame, text and gaps included, and produces 21 model inputs: eight scaled numbers, the imputed satisfaction score and its missing flag, eight country columns and three segment columns. <code dir=\"ltr\">get_feature_names_out</code> lists them with a prefix naming the transformer that made each one, which is how you match coefficients to columns later.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">More columns, worse model</h2>\n<p>Then the surprise: the full pipeline ranks customers worse than the eight numeric columns alone, 0.776 against 0.807. Eleven one-hot columns and a satisfaction score with half its values imputed give logistic regression more coefficients to fit with only 298 rows, and on this data the extra columns carry more noise than signal. Egypt’s churn rate does look higher in the training data, but with 44 Egyptian customers in training that’s too thin to bet on.</p>\n<p>That’s not a failure of the pipeline. It’s the pipeline doing its job: it made testing the extra columns a five-line change, and cross-validation gave a clear answer. For now, the numeric model stays the champion. The categorical columns are worth revisiting when Cartwheel has a few thousand customers, or with stronger regularisation.</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>Preprocessing before you split</span></p>\n<p><code dir=\"ltr\">StandardScaler().fit_transform(X)</code> on the full table, then <code dir=\"ltr\">train_test_split</code> or <code dir=\"ltr\">cross_val_score</code>, means the validation rows helped compute the means and standard deviations. With scaling the leak is small; with imputation, target encoding or feature selection it can be large. Put every learned preprocessing step inside the pipeline and pass raw data to it.</p>\n</aside>\n<aside class=\"callout callout-tip\" 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=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Reach into a fitted pipeline</span></p>\n<p><code dir=\"ltr\">pipe.named_steps[&quot;model&quot;].coef_</code> (or <code dir=\"ltr\">pipe[-1].coef_</code>) gets the model; <code dir=\"ltr\">pipe.named_steps[&quot;prep&quot;].named_transformers_[&quot;cat&quot;].categories_</code> shows the categories the encoder learned. Pipelines aren’t black boxes; they’re containers you can open.</p>\n</aside>\n<p>A pipeline also gives every setting an address: <code dir=\"ltr\">model__C</code> is the <code dir=\"ltr\">C</code> of the step named <code dir=\"ltr\">model</code>, and <code dir=\"ltr\">prep__sat__simpleimputer__strategy</code> reaches the imputer. Next lesson uses those addresses to search for the best settings automatically.</p>\n","toc":[{"id":"sec-1","text":"Pipelines bundle steps into one estimator","level":2},{"id":"sec-2","text":"Different columns, different treatment","level":2},{"id":"sec-3","text":"More columns, worse model","level":2}],"takeaways":["A <code dir=\"ltr\">Pipeline</code> chains transformers and a final model into one estimator: <code dir=\"ltr\">fit</code> fits every step in order, <code dir=\"ltr\">predict</code> runs new data through the same steps.","<code dir=\"ltr\">ColumnTransformer</code> applies different preprocessing to different columns and concatenates the results.","<code dir=\"ltr\">OneHotEncoder(handle_unknown=&quot;ignore&quot;)</code> turns categories into 0/1 columns and won’t crash on a category it never saw in training.","<code dir=\"ltr\">SimpleImputer(add_indicator=True)</code> fills gaps and adds a column recording which values were missing.","Because the whole pipeline is refit in each fold, preprocessing only ever learns from training rows, which keeps cross-validation honest."],"further":[{"title":"Pipelines and composite estimators","url":"https://scikit-learn.org/stable/modules/compose.html"},{"title":"ColumnTransformer","url":"https://scikit-learn.org/stable/modules/generated/sklearn.compose.ColumnTransformer.html"},{"title":"OneHotEncoder","url":"https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html"},{"title":"Imputation of missing values","url":"https://scikit-learn.org/stable/modules/impute.html"}],"quiz":[{"q":"<p>During <code dir=\"ltr\">cross_validate(pipe, X_train, y_train, cv=5)</code>, where does the <code dir=\"ltr\">StandardScaler</code> inside <code dir=\"ltr\">pipe</code> get its means from?</p>","options":[{"text":"From the four training folds of each round only.","why":"Correct. The whole pipeline is cloned and refit per fold, so the validation fold never influences preprocessing."},{"text":"From all of <code dir=\"ltr\">X_train</code>, computed once before the folds are made.","why":"That’s what happens if you scale before cross-validating, and it’s the leak pipelines exist to prevent."},{"text":"From the validation fold, so validation data is centred correctly.","why":"Fitting anything on the validation fold would leak it into the model."},{"text":"From the test set, since that’s what will be predicted.","why":"The test set isn’t passed to <code dir=\"ltr\">cross_validate</code> at all."}],"answer":0},{"q":"<p>A customer from France (absent in training) reaches a pipeline whose <code dir=\"ltr\">OneHotEncoder</code> uses the default <code dir=\"ltr\">handle_unknown</code>. What happens?</p>","options":[{"text":"The encoder maps France to the most common country.","why":"No encoder in scikit-learn silently substitutes a category like that."},{"text":"The customer’s country columns are all zero and prediction continues.","why":"That’s the behaviour with <code dir=\"ltr\">handle_unknown=&quot;ignore&quot;</code>, not the default."},{"text":"The encoder adds a new France column on the fly.","why":"The column layout is fixed at fit time; the model’s coefficients couldn’t use a new column anyway."},{"text":"<code dir=\"ltr\">transform</code> raises <code dir=\"ltr\">ValueError: Found unknown categories ['France']</code>.","why":"Correct. The default is <code dir=\"ltr\">&quot;error&quot;</code>. Use <code dir=\"ltr\">handle_unknown=&quot;ignore&quot;</code> for anything that will see live data."}],"answer":3},{"q":"<p>Why use <code dir=\"ltr\">SimpleImputer(strategy=&quot;median&quot;, add_indicator=True)</code> for <code dir=\"ltr\">avg_satisfaction</code>?</p>","options":[{"text":"The indicator column lets the model learn that a missing survey is itself informative, while the median fills the gap.","why":"Correct. Missingness is often a signal (customers who never answer surveys may behave differently)."},{"text":"The median makes the column’s distribution normal.","why":"Filling with the median doesn’t change the shape of the observed values; it only fills gaps."},{"text":"The indicator replaces the original column, so the model ignores satisfaction.","why":"The indicator is added alongside the imputed column, not instead of it."}],"answer":0},{"q":"<p>Adding <code dir=\"ltr\">country</code> and <code dir=\"ltr\">segment</code> to the logistic pipeline lowers cross-validated ROC AUC from 0.807 to 0.776. What’s the best interpretation?</p>","options":[{"text":"The OneHotEncoder is misconfigured, because more information can’t hurt.","why":"Extra features can hurt; each one adds coefficients that can fit noise, especially with a few hundred rows."},{"text":"On 298 rows, eleven extra one-hot columns add more noise than signal, so leave them out for now.","why":"Correct. Re-test them when there’s more data or a stronger penalty."},{"text":"Country causes churn in a way logistic regression can’t capture.","why":"Nothing here supports a causal claim, and the drop says the columns aren’t helping this model."}],"answer":1}],"exercise":{"kind":"python","files":["churn.csv"],"starter":"import numpy as np\nimport pandas as pd\nfrom sklearn.compose import ColumnTransformer\nfrom sklearn.impute import SimpleImputer\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.model_selection import StratifiedKFold, cross_val_score, train_test_split\nfrom sklearn.pipeline import Pipeline, make_pipeline\nfrom sklearn.preprocessing import OneHotEncoder, StandardScaler\n\nchurn = pd.read_csv(\"churn.csv\")\nX = churn.drop(columns=[\"customer_id\", \"churned\"])\ny = churn[\"churned\"]\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)\nnumeric = [\"tenure_days\", \"orders\", \"total_spent\", \"avg_order_value\",\n           \"days_since_last_order\", \"used_coupon\", \"support_tickets\", \"mobile_share\"]\n\n# A customer the training data never prepared you for\nnew_customer = pd.DataFrame([{\n    \"country\": \"France\", \"segment\": \"government\", \"tenure_days\": 200, \"orders\": 2,\n    \"total_spent\": 150.0, \"avg_order_value\": 75.0, \"days_since_last_order\": 100,\n    \"used_coupon\": 0, \"support_tickets\": 0, \"avg_satisfaction\": np.nan, \"mobile_share\": 0.5}])\n\n# 1. Preprocess: scale `numeric`; impute (median, with indicator) then scale avg_satisfaction;\n#    one-hot encode `segment` so unseen categories don't crash\npreprocess = ColumnTransformer([\n    (\"num\", StandardScaler(), numeric),\n    (\"sat\", StandardScaler(), [\"avg_satisfaction\"]),\n    (\"cat\", OneHotEncoder(), [\"segment\"]),\n])\n\n# 2. Pipeline with steps named \"prep\" and \"model\" (LogisticRegression), fitted on the training data\npipe = Pipeline([(\"prep\", preprocess), (\"model\", LogisticRegression())])\n\n# 3. Mean cross-validated ROC AUC on the training data\ncv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\ncv_auc = None\n\n# 4. Churn probability for new_customer\nnew_prob = None\n","solution":"import numpy as np\nimport pandas as pd\nfrom sklearn.compose import ColumnTransformer\nfrom sklearn.impute import SimpleImputer\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.model_selection import StratifiedKFold, cross_val_score, train_test_split\nfrom sklearn.pipeline import Pipeline, make_pipeline\nfrom sklearn.preprocessing import OneHotEncoder, StandardScaler\n\nchurn = pd.read_csv(\"churn.csv\")\nX = churn.drop(columns=[\"customer_id\", \"churned\"])\ny = churn[\"churned\"]\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)\nnumeric = [\"tenure_days\", \"orders\", \"total_spent\", \"avg_order_value\",\n           \"days_since_last_order\", \"used_coupon\", \"support_tickets\", \"mobile_share\"]\n\n# A customer the training data never prepared you for\nnew_customer = pd.DataFrame([{\n    \"country\": \"France\", \"segment\": \"government\", \"tenure_days\": 200, \"orders\": 2,\n    \"total_spent\": 150.0, \"avg_order_value\": 75.0, \"days_since_last_order\": 100,\n    \"used_coupon\": 0, \"support_tickets\": 0, \"avg_satisfaction\": np.nan, \"mobile_share\": 0.5}])\n\npreprocess = ColumnTransformer([\n    (\"num\", StandardScaler(), numeric),\n    (\"sat\", make_pipeline(SimpleImputer(strategy=\"median\", add_indicator=True), StandardScaler()),\n     [\"avg_satisfaction\"]),\n    (\"cat\", OneHotEncoder(handle_unknown=\"ignore\"), [\"segment\"]),\n])\npipe = Pipeline([(\"prep\", preprocess), (\"model\", LogisticRegression())])\npipe.fit(X_train, y_train)\n\ncv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\ncv_auc = cross_val_score(pipe, X_train, y_train, cv=cv, scoring=\"roc_auc\").mean()\n\nnew_prob = pipe.predict_proba(new_customer)[0, 1]\nprint(round(cv_auc, 3), round(new_prob, 3))\n","checks":[{"id":"steps","test":"list(pipe.named_steps) == ['prep', 'model'] and type(pipe.named_steps['model']).__name__ == 'LogisticRegression'"},{"id":"fitted","test":"len(pipe.named_steps['prep'].get_feature_names_out()) == 13"},{"id":"cv-auc","test":"cv_auc is not None and 0.75 < float(cv_auc) < 0.85"},{"id":"unseen","test":"new_prob is not None and 0.0 < float(new_prob) < 1.0 and abs(float(new_prob) - float(pipe.predict_proba(new_customer)[0, 1])) < 1e-9"}],"title":"A pipeline that survives real data","prompt":"<p>Make the starter pipeline handle gaps and surprises.</p>\n<ol>\n<li><code dir=\"ltr\">preprocess</code>: scale the <code dir=\"ltr\">numeric</code> columns; for <code dir=\"ltr\">avg_satisfaction</code>, impute with the median <strong>and add a missing indicator</strong>, then scale; one-hot encode <code dir=\"ltr\">segment</code> so that unseen categories are ignored instead of raising an error.</li>\n<li><code dir=\"ltr\">pipe</code>: steps named <code dir=\"ltr\">&quot;prep&quot;</code> and <code dir=\"ltr\">&quot;model&quot;</code> (a <code dir=\"ltr\">LogisticRegression</code>), fitted on the training data. It should produce 13 model inputs.</li>\n<li><code dir=\"ltr\">cv_auc</code>: mean ROC AUC from <code dir=\"ltr\">cross_val_score</code> with the provided <code dir=\"ltr\">cv</code>.</li>\n<li><code dir=\"ltr\">new_prob</code>: the churn probability for <code dir=\"ltr\">new_customer</code>, who comes from an unseen country, an unseen segment and has no satisfaction score.</li>\n</ol>\n","hints":["<code dir=\"ltr\">StandardScaler</code> alone can’t handle NaN. Chain <code dir=\"ltr\">make_pipeline(SimpleImputer(strategy=&quot;median&quot;, add_indicator=True), StandardScaler())</code>.","<code dir=\"ltr\">OneHotEncoder(handle_unknown=&quot;ignore&quot;)</code>. The country column isn’t listed in the transformer, so it’s dropped.","Don’t forget <code dir=\"ltr\">pipe.fit(X_train, y_train)</code>; then <code dir=\"ltr\">pipe.predict_proba(new_customer)[0, 1]</code>."],"explanation":"<p>Eight scaled numeric columns, the imputed satisfaction score plus its missing indicator, and three segment columns make 13 inputs. Columns a <code dir=\"ltr\">ColumnTransformer</code> doesn’t mention (here <code dir=\"ltr\">country</code>) are dropped by default (<code dir=\"ltr\">remainder=&quot;drop&quot;</code>). The new customer gets all-zero segment columns and a median satisfaction with the indicator set to 1, so the model still produces a probability. Without <code dir=\"ltr\">handle_unknown=&quot;ignore&quot;</code> and the imputer, this one customer would crash a production service.</p>\n","checkLabels":{"steps":"pipe has steps named prep and model","fitted":"The fitted preprocessor produces 13 inputs","cv-auc":"cv_auc is the mean cross-validated ROC AUC","unseen":"new_prob is a valid probability for the unseen customer"}},"words":676,"readMinutes":3}