{"id":"l-ml-4-5","courseId":"ml-crash-course","lang":"en","kind":"lesson","summary":"Turn domain knowledge into rates, ratios and log transforms, package them in a FunctionTransformer inside the pipeline, and keep only the features that beat cross-validation noise.","html":"<p>Models only see the columns you give them. A support lead looking at a customer doesn’t think “4 orders and 600 days of tenure”; they think “orders about once every five months”. That rate, not either raw number, is the useful idea. <strong>Feature engineering</strong> turns that kind of domain knowledge into columns, and on tabular data it’s often worth more than any amount of tuning.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Ideas that come from the business</h2>\n<p>Good churn features usually fall into a few families:</p>\n<ul>\n<li><strong>Rates</strong>: orders per month of tenure, tickets per order. Raw counts grow with tenure; rates compare customers fairly.</li>\n<li><strong>Ratios against the customer’s own habits</strong>: days since last order divided by their typical gap between orders. Thirty days of silence is alarming for a weekly buyer and normal for someone who orders twice a year.</li>\n<li><strong>Transforms</strong>: <code dir=\"ltr\">log1p</code> of money columns, which squeezes long right tails so that the difference between $50 and $150 counts for more than the difference between $3,000 and $3,100.</li>\n<li><strong>Flags</strong>: “never answered the survey”, “only ever ordered once”.</li>\n</ul>\n<p>Each of these must be computable from information available at the snapshot date. A rate built from tenure and orders is fine; anything that peeks into the outcome window is the leak from last lesson.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Features inside the pipeline</h2>\n<p>Write the feature code once, as a function from DataFrame to DataFrame, and wrap it in a <code dir=\"ltr\">FunctionTransformer</code>. Inside a pipeline it runs in cross-validation, in the final fit and in production, always the same way.</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\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> numpy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> np</span></span>\n<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.preprocessing </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> FunctionTransformer</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\">features </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>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">def</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> add_features</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(df):</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df.copy()</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    months </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tenure_days\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 30.4</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders_per_month\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> months</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"typical_gap\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tenure_days\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"overdue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"days_since_last_order\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"typical_gap\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"log_spent\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> np.log1p(df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total_spent\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">engineered </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> FunctionTransformer(add_features).fit_transform(churn[features])</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(engineered[[</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\">\"tenure_days\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders_per_month\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"overdue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"log_spent\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]].head().round(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</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\">\"skew of total_spent:\"</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\">(churn[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total_spent\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].skew(), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">      \"| after log1p:\"</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\">(np.log1p(churn[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total_spent\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]).skew(), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</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><code dir=\"ltr\">df.copy()</code> matters: the function shouldn’t modify the caller’s DataFrame. Since every customer has at least one order and positive tenure, the divisions are safe here; in your own data, guard against zeros.</p>\n<p>The function is <strong>stateless</strong>: each row’s new values depend only on that row. Stateless features can’t leak across folds. A feature like “spend compared with the average customer” is different, because the average is learned from data; it needs a proper transformer with <code dir=\"ltr\">fit</code>, or it will quietly use validation rows.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Do they help? Ask cross-validation</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\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> numpy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> np</span></span>\n<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.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\"> 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\"> FunctionTransformer, StandardScaler</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.tree </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> DecisionTreeClassifier</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\">features </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\">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(</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    churn[features], churn[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churned\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], </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:#953800;--shiki-dark:#FFA657\">    stratify</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\">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>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">def</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> add_features</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(df):</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df.copy()</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders_per_month\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tenure_days\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 30.4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"typical_gap\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tenure_days\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"overdue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"days_since_last_order\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"typical_gap\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"log_spent\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> np.log1p(df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total_spent\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">models </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tree\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: DecisionTreeClassifier(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">min_samples_leaf</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">20</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:#0A3069;--shiki-dark:#A5D6FF\">          \"logistic\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: make_pipeline(StandardScaler(), LogisticRegression())}</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> name, model </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> models.items():</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    plain </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cross_val_score(model, 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\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    richer </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cross_val_score(make_pipeline(FunctionTransformer(add_features), model),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">                             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\">)</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\">\"</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:8}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> plain </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">plain.mean()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.3f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ± </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">plain.std()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.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\">\"engineered </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">richer.mean()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.3f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ± </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">richer.std()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.3f}</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>The single tree improves from 0.716 to 0.759, and its folds become steadier. Look inside and you’ll see why: <code dir=\"ltr\">orders_per_month</code> becomes the root question. A tree can only compare one column with a threshold, so “few orders for this much tenure” would take a staircase of splits to imitate. Handed the ratio, it needs one.</p>\n<p>Logistic regression moves from 0.807 to 0.811, well inside a spread of ±0.04. That’s no change. A linear model can already weigh <code dir=\"ltr\">orders</code> against <code dir=\"ltr\">tenure_days</code>, and recency, the dominant signal, was there from the start. Different model families need different features: ratios and interactions for trees, straightening transforms for linear models.</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>Keeping every feature that nudges the mean up</span></p>\n<p>With five folds of 60 customers, the mean AUC wobbles by a point or more from noise alone. If you try twenty features and keep each one that raises the mean, you’ll keep about half of them by luck and ship a slower, harder-to-explain model. Keep a feature when its gain clearly exceeds the spread, or when it carries a business meaning you need anyway.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Closing the section: the champion</h2>\n<p>Section 4 tested ensembles, extra columns, tuning and engineered features with honest cross-validation. Most of them didn’t beat the simple model. So the champion is still the scaled logistic regression on eight numeric columns, and now it’s time to spend the test set:</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\">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.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.metrics </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> roc_auc_score</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\"> 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\">features </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\">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(</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    churn[features], churn[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churned\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], </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:#953800;--shiki-dark:#FFA657\">    stratify</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>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">champion </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline(StandardScaler(), LogisticRegression()).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\">\"test ROC AUC:\"</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\">(roc_auc_score(y_test, champion.predict_proba(X_test)[:, </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\">3</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\">\"test accuracy:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, champion.score(X_test, y_test))</span></span></code></pre><div class=\"run-output\" data-run-output=\"2\" hidden></div></div>\n<p>Test ROC AUC 0.81 against a cross-validated 0.81: no surprise, which is exactly what a leak-free process looks like. That agreement is what section 4 bought you. Not a higher number, but a number you can stand behind in a meeting.</p>\n<aside class=\"callout callout-why\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">Why this matters</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Why “the simple model won” is a good outcome</span></p>\n<p>A model you can explain in two sentences, retrain in milliseconds and debug with a coefficient table is cheaper to run for years. When the data grows, rerun the same comparisons; the ensembles and the categorical columns may earn their place then. The process, not the winner, is the reusable asset.</p>\n</aside>\n<p>Section 5 steps beyond supervised tables: finding structure without labels, a first look at neural networks, where LLMs fit in, and what it takes to put the champion in front of real customers responsibly.</p>\n","toc":[{"id":"sec-1","text":"Ideas that come from the business","level":2},{"id":"sec-2","text":"Features inside the pipeline","level":2},{"id":"sec-3","text":"Do they help? Ask cross-validation","level":2},{"id":"sec-4","text":"Closing the section: the champion","level":2}],"takeaways":["Good features express what an expert would look at: rates per unit of time, ratios between related columns, and flags for meaningful states.","Trees split one column at a time, so they benefit most from ratio features that combine columns; linear models benefit from transforms like logs that straighten relationships.","Put feature code in a <code dir=\"ltr\">FunctionTransformer</code> inside the pipeline so training, cross-validation and production compute features identically.","A new feature earns its place only if the cross-validated gain is larger than the fold-to-fold spread.","After choosing, refit the champion on all training data and score it once on the test set."],"further":[{"title":"FunctionTransformer","url":"https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.FunctionTransformer.html"},{"title":"Preprocessing data: custom transformers","url":"https://scikit-learn.org/stable/modules/preprocessing.html#custom-transformers"}],"quiz":[{"q":"<p>Adding <code dir=\"ltr\">orders_per_month</code> lifts a single tree’s CV AUC from 0.72 to 0.76 but leaves logistic regression unchanged at 0.81. What’s the most likely reason?</p>","options":[{"text":"The tree couldn’t form “orders relative to tenure” from its one-column-at-a-time splits, while the linear model could already approximate it by weighting both columns.","why":"Correct. A tree needs many splits to imitate a ratio; handing it the ratio directly gives it one clean question."},{"text":"Logistic regression ignores new features after the first eight.","why":"It uses every column it receives; the new one simply adds little beyond what it already had."},{"text":"The tree is overfitting the new feature.","why":"Overfitting would hurt validation scores, not improve them."},{"text":"The ratio leaks the target.","why":"Orders and tenure are both known at the snapshot date, so their ratio is safe."}],"answer":0},{"q":"<p>Why compute features inside a <code dir=\"ltr\">FunctionTransformer</code> in the pipeline instead of adding columns to the DataFrame up front?</p>","options":[{"text":"<code dir=\"ltr\">FunctionTransformer</code> makes features faster to compute.","why":"Speed is the same; it’s the same pandas code either way."},{"text":"The production service then computes exactly the same features from raw input, with no separate code to keep in sync.","why":"Correct. One object holds the feature logic and the model, so they can’t drift apart."},{"text":"Features added to a DataFrame can’t be used by scikit-learn models.","why":"Any numeric DataFrame column can be used; the benefit is consistency."}],"answer":1},{"q":"<p><code dir=\"ltr\">total_spent</code> is strongly right-skewed (a few customers spent thousands). Which transform usually helps a linear model?</p>","options":[{"text":"Squaring it, to spread the large values further.","why":"Squaring makes the skew worse and gives the extreme customers even more influence."},{"text":"One-hot encoding it.","why":"That treats every distinct amount as a separate category, which throws away order and explodes the column count."},{"text":"<code dir=\"ltr\">np.log1p(total_spent)</code>, which compresses large values so a $100 difference matters more at $50 than at $3,000.","why":"Correct. Log transforms tame skew and often make relationships closer to linear. Trees don’t need them."}],"answer":2},{"q":"<p>A new feature raises mean CV AUC from 0.807 to 0.811, with fold standard deviations around 0.035. What should you do?</p>","options":[{"text":"Keep it; any improvement is worth having.","why":"Every feature has a running cost (computing, monitoring, explaining), and a gain this size is indistinguishable from noise."},{"text":"Report it as a 0.4-point improvement.","why":"That overstates evidence that isn’t there."},{"text":"Tune the model again until the gap is bigger.","why":"Tuning to widen a noisy gap is overfitting your validation folds."},{"text":"Treat it as no change; keep the simpler feature set unless the feature has another reason to exist.","why":"Correct. A gain well inside the spread is noise; the default is to leave the feature out."}],"answer":3}],"exercise":{"kind":"python","files":["churn.csv"],"starter":"import numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import StratifiedKFold, cross_val_score, train_test_split\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.preprocessing import FunctionTransformer\nfrom sklearn.tree import DecisionTreeClassifier\n\nchurn = pd.read_csv(\"churn.csv\")\nfeatures = [\"tenure_days\", \"orders\", \"total_spent\", \"avg_order_value\",\n            \"days_since_last_order\", \"used_coupon\", \"support_tickets\", \"mobile_share\"]\nX_train, X_test, y_train, y_test = train_test_split(\n    churn[features], churn[\"churned\"], test_size=0.25, random_state=42,\n    stratify=churn[\"churned\"])\ncv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\ntree = DecisionTreeClassifier(min_samples_leaf=20, random_state=42)\n\n# 1. Return a COPY of df with two new columns:\n#    orders_per_month = orders / (tenure_days / 30.4)\n#    tickets_per_order = support_tickets / orders\ndef add_features(df):\n    return df\n\n# 2. Pipeline: FunctionTransformer(add_features), then `tree`\npipe = make_pipeline(tree)\n\n# 3. Mean CV ROC AUC on the training data, for the plain tree and for your pipeline\nplain_auc = None\nengineered_auc = None\n","solution":"import numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import StratifiedKFold, cross_val_score, train_test_split\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.preprocessing import FunctionTransformer\nfrom sklearn.tree import DecisionTreeClassifier\n\nchurn = pd.read_csv(\"churn.csv\")\nfeatures = [\"tenure_days\", \"orders\", \"total_spent\", \"avg_order_value\",\n            \"days_since_last_order\", \"used_coupon\", \"support_tickets\", \"mobile_share\"]\nX_train, X_test, y_train, y_test = train_test_split(\n    churn[features], churn[\"churned\"], test_size=0.25, random_state=42,\n    stratify=churn[\"churned\"])\ncv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\ntree = DecisionTreeClassifier(min_samples_leaf=20, random_state=42)\n\ndef add_features(df):\n    df = df.copy()\n    df[\"orders_per_month\"] = df[\"orders\"] / (df[\"tenure_days\"] / 30.4)\n    df[\"tickets_per_order\"] = df[\"support_tickets\"] / df[\"orders\"]\n    return df\n\npipe = make_pipeline(FunctionTransformer(add_features), tree)\n\nplain_auc = cross_val_score(tree, X_train, y_train, cv=cv, scoring=\"roc_auc\").mean()\nengineered_auc = cross_val_score(pipe, X_train, y_train, cv=cv, scoring=\"roc_auc\").mean()\nprint(round(plain_auc, 3), round(engineered_auc, 3))\n","checks":[{"id":"new-columns","test":"list(add_features(X_train.head(3)).columns) == features + ['orders_per_month', 'tickets_per_order'] or set(add_features(X_train.head(3)).columns) == set(features) | {'orders_per_month', 'tickets_per_order'}"},{"id":"values","test":"abs(float(add_features(X_train)['orders_per_month'].iloc[0]) - float(X_train['orders'].iloc[0] / (X_train['tenure_days'].iloc[0] / 30.4))) < 1e-9 and abs(float(add_features(X_train)['tickets_per_order'].iloc[0]) - float(X_train['support_tickets'].iloc[0] / X_train['orders'].iloc[0])) < 1e-9"},{"id":"no-mutation","test":"list(X_train.columns) == features"},{"id":"pipeline","test":"type(pipe[0]).__name__ == 'FunctionTransformer' and type(pipe[-1]).__name__ == 'DecisionTreeClassifier'"},{"id":"scores","test":"plain_auc is not None and engineered_auc is not None and abs(float(plain_auc) - 0.716) < 0.002 and float(engineered_auc) > float(plain_auc) + 0.02"}],"title":"Give the tree a rate to split on","prompt":"<p>Help a single decision tree with two engineered features.</p>\n<ol>\n<li>Complete <code dir=\"ltr\">add_features(df)</code> so it returns a <strong>copy</strong> of <code dir=\"ltr\">df</code> with <code dir=\"ltr\">orders_per_month</code> (<code dir=\"ltr\">orders / (tenure_days / 30.4)</code>) and <code dir=\"ltr\">tickets_per_order</code> (<code dir=\"ltr\">support_tickets / orders</code>) added. Don’t modify the DataFrame passed in.</li>\n<li><code dir=\"ltr\">pipe</code>: <code dir=\"ltr\">FunctionTransformer(add_features)</code> followed by <code dir=\"ltr\">tree</code>.</li>\n<li><code dir=\"ltr\">plain_auc</code> and <code dir=\"ltr\">engineered_auc</code>: mean cross-validated ROC AUC on the training data (with the provided <code dir=\"ltr\">cv</code>) for <code dir=\"ltr\">tree</code> alone and for <code dir=\"ltr\">pipe</code>.</li>\n</ol>\n","hints":["Start the function with <code dir=\"ltr\">df = df.copy()</code> so the caller’s data is untouched.","<code dir=\"ltr\">make_pipeline(FunctionTransformer(add_features), tree)</code>","<code dir=\"ltr\">cross_val_score(model, X_train, y_train, cv=cv, scoring=&quot;roc_auc&quot;).mean()</code> for each."],"explanation":"<p>The plain tree averages about 0.72; with the two rates it climbs by several points, mostly thanks to <code dir=\"ltr\">orders_per_month</code>, which becomes the tree’s first question. Because the features live in the pipeline, cross-validation and any later prediction compute them the same way, and since each value depends only on its own row, nothing leaks between folds. Run the same comparison with logistic regression and you’d see almost no change: different models need different help.</p>\n","checkLabels":{"new-columns":"add_features adds both new columns","values":"The new columns hold the right values","no-mutation":"X_train itself is not modified","pipeline":"pipe is FunctionTransformer → DecisionTreeClassifier","scores":"Both CV scores are computed, and the engineered tree beats the plain one"}},"words":710,"readMinutes":3}