{"id":"l-ml-4-5","courseId":"ml-crash-course","lang":"ar","kind":"lesson","summary":"حوّل المعرفة بمجال العمل إلى معدّلات ونِسب وتحويلات لوغاريتمية، وضعها في FunctionTransformer داخل الـ pipeline، واحتفظ فقط بالـ features التي تتجاوز ضجيج التحقق المتقاطع.","html":"<p>النماذج لا ترى إلا الأعمدة التي تعطيها إياها. وقائد فريق الدعم حين ينظر إلى عميل لا يفكّر “4 طلبات و600 يوم من العضوية”؛ بل يفكّر “طلب كل خمسة أشهر أو نحوها”. هذا المعدّل، لا أيٌّ من الرقمين الخامين، هو الفكرة المفيدة. و<strong>هندسة الـ features (feature engineering)</strong> تحوّل هذا النوع من المعرفة بمجال العمل إلى أعمدة، وفي البيانات الجدولية كثيرًا ما تساوي أكثر من أي قدر من الضبط.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">أفكار تأتي من مجال العمل</h2>\n<p>features الانقطاع الجيدة تقع عادةً في بضع عائلات:</p>\n<ul>\n<li><strong>المعدّلات</strong>: الطلبات لكل شهر عضوية، والتذاكر لكل طلب. الأعداد الخام تكبر مع مدة العضوية؛ أما المعدّلات فتقارن العملاء بإنصاف.</li>\n<li><strong>النِّسب مقارنةً بعادات العميل نفسه</strong>: الأيام منذ آخر طلب مقسومة على الفجوة المعتادة بين طلباته. ثلاثون يومًا من الصمت مقلقة لمشترٍ أسبوعي، وطبيعية لمن يطلب مرتين في السنة.</li>\n<li><strong>التحويلات</strong>: <code dir=\"ltr\">log1p</code> لأعمدة المال، الذي يضغط الذيول الطويلة نحو اليمين بحيث يُحسب الفرق بين 50 و150 دولارًا أكثر من الفرق بين 3,000 و3,100 دولار.</li>\n<li><strong>العلامات (flags)</strong>: “لم يُجب عن الاستبيان قط”، و&quot;لم يطلب في حياته إلا مرة واحدة&quot;.</li>\n</ul>\n<p>كلٌّ من هذه يجب أن يكون قابلًا للحساب من معلومات متاحة في تاريخ اللقطة. المعدّل المبني من مدة العضوية والطلبات سليم؛ وأي شيء يختلس النظر إلى نافذة النتيجة هو التسرّب الذي تحدّثنا عنه في الدرس السابق.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">الـ Features داخل الـ pipeline</h2>\n<p>اكتب كود الـ features مرة واحدة، كدالة من DataFrame إلى DataFrame، وغلّفها في <code dir=\"ltr\">FunctionTransformer</code>. داخل الـ pipeline تعمل في التحقق المتقاطع، وفي التدريب النهائي، وفي بيئة الإنتاج، دائمًا بالطريقة نفسها.</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=\"شغّل هذا المثال\"><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>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><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>نسخ</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> مهم: لا ينبغي للدالة أن تعدّل الـ DataFrame الخاص بمن استدعاها. ولأن لكل عميل طلبًا واحدًا على الأقل ومدة عضوية موجبة، فالقسمة آمنة هنا؛ أما في بياناتك أنت، فاحترس من الأصفار.</p>\n<p>الدالة <strong>عديمة الحالة (stateless)</strong>: القيم الجديدة لكل صف تعتمد على ذلك الصف وحده. والـ features عديمة الحالة لا يمكن أن تتسرّب بين الطيّات. أما feature مثل “الإنفاق مقارنةً بمتوسط العملاء” فمختلف، لأن المتوسط يُتعلَّم من البيانات؛ فهو يحتاج إلى محوّل حقيقي له <code dir=\"ltr\">fit</code>، وإلا استخدم صفوف التحقق بصمت.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">هل تساعد؟ اسأل التحقق المتقاطع</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=\"شغّل هذا المثال\"><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>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><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>نسخ</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>تتحسّن الشجرة الواحدة من 0.716 إلى 0.759، وتصبح طيّاتها أكثر ثباتًا. انظر إلى داخلها وسترى السبب: يصبح <code dir=\"ltr\">orders_per_month</code> سؤال الجذر. لا تستطيع الشجرة إلا مقارنة عمود واحد بعتبة، فـ “طلبات قليلة بالنسبة إلى مدة العضوية هذه” كانت ستحتاج إلى سلّم من التقسيمات لمحاكاتها. أما حين تُسلَّم النسبة، فيكفيها تقسيم واحد.</p>\n<p>ويتحرك الانحدار اللوجستي من 0.807 إلى 0.811، وهذا ضمن تشتّت ±0.04 بوضوح. أي لا تغيير. فالنموذج الخطي يستطيع أصلًا أن يوازن <code dir=\"ltr\">orders</code> مقابل <code dir=\"ltr\">tenure_days</code>، وحداثة آخر طلب، الإشارة المهيمنة، موجودة منذ البداية. عائلات النماذج المختلفة تحتاج إلى features مختلفة: النِّسب والتفاعلات للأشجار، والتحويلات المقوِّمة للنماذج الخطية.</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\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الاحتفاظ بكل feature يدفع المتوسط قليلًا إلى الأعلى</span></p>\n<p>مع خمس طيّات من 60 عميلًا، يتأرجح متوسط الـ AUC بنقطة أو أكثر من الضجيج وحده. فإذا جرّبت عشرين feature واحتفظت بكل ما يرفع المتوسط، فستحتفظ بنحو نصفها بالحظ، وتطلق نموذجًا أبطأ وأصعب في الشرح. احتفظ بالـ feature حين يتجاوز مكسبه التشتّت بوضوح، أو حين يحمل معنى عمليًا تحتاج إليه على أي حال.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">ختام القسم: البطل</h2>\n<p>اختبر القسم 4 النماذج المجمّعة والأعمدة الإضافية والضبط والـ features المهندَسة بتحقق متقاطع أمين. ومعظمها لم يتفوّق على النموذج البسيط. لذا يبقى البطل هو الانحدار اللوجستي بعد توحيد المقاييس على ثمانية أعمدة رقمية، وقد حان الوقت لإنفاق بيانات الاختبار:</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=\"شغّل هذا المثال\"><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>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><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>نسخ</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>ROC AUC على الاختبار بقيمة 0.81 مقابل 0.81 بالتحقق المتقاطع: لا مفاجأة، وهذا بالضبط ما تبدو عليه عملية خالية من التسرّب. هذا التوافق هو ما اشتراه لك القسم 4. ليس رقمًا أعلى، بل رقمًا تستطيع أن تقف خلفه في اجتماع.</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\">لماذا يهمّ هذا</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>لماذا يُعدّ “فوز النموذج البسيط” نتيجة جيدة</span></p>\n<p>النموذج الذي تستطيع شرحه في جملتين، وإعادة تدريبه في أجزاء من الثانية، وتتبّع أخطائه بجدول معاملات، أرخص في التشغيل لسنوات. وحين تكبر البيانات، أعد إجراء المقارنات نفسها؛ فقد تستحق النماذج المجمّعة والأعمدة الفئوية مكانها حينها. العملية، لا الفائز، هي الأصل القابل لإعادة الاستخدام.</p>\n</aside>\n<p>يخطو القسم 5 إلى ما هو أبعد من الجداول الموجَّهة: إيجاد البنية دون labels، ونظرة أولى على الشبكات العصبية، وموقع النماذج اللغوية الكبيرة، وما يتطلّبه وضع البطل أمام عملاء حقيقيين بمسؤولية.</p>\n","toc":[{"id":"sec-1","text":"أفكار تأتي من مجال العمل","level":2},{"id":"sec-2","text":"الـ Features داخل الـ pipeline","level":2},{"id":"sec-3","text":"هل تساعد؟ اسأل التحقق المتقاطع","level":2},{"id":"sec-4","text":"ختام القسم: البطل","level":2}],"takeaways":["الـ features الجيدة تعبّر عمّا ينظر إليه الخبير: معدّلات لكل وحدة زمن، ونِسب بين أعمدة مترابطة، وعلامات لحالات ذات معنى.","تقسم الأشجار على عمود واحد في كل مرة، فهي الأكثر استفادة من features النِّسب التي تجمع الأعمدة؛ أما النماذج الخطية فتستفيد من تحويلات مثل اللوغاريتم التي تقوّم العلاقات.","ضع كود الـ features في <code dir=\"ltr\">FunctionTransformer</code> داخل الـ pipeline، كي يحسب التدريب والتحقق المتقاطع وبيئة الإنتاج الـ features بالطريقة نفسها تمامًا.","لا يستحق الـ feature الجديد مكانه إلا إذا كان مكسبه في التحقق المتقاطع أكبر من التشتّت بين الطيّات.","بعد الاختيار، أعد تدريب البطل على بيانات التدريب كلها وقيّمه مرة واحدة على بيانات الاختبار."],"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>إضافة <code dir=\"ltr\">orders_per_month</code> ترفع الـ AUC بالتحقق المتقاطع لشجرة واحدة من 0.72 إلى 0.76، لكنها تُبقي الانحدار اللوجستي على حاله عند 0.81. ما السبب الأرجح؟</p>","options":[{"text":"لم تستطع الشجرة تكوين “الطلبات نسبةً إلى مدة العضوية” بتقسيماتها على عمود واحد في كل مرة، بينما كان النموذج الخطي يستطيع مقاربتها أصلًا بوزن العمودين.","why":"صحيح. تحتاج الشجرة إلى تقسيمات كثيرة لتحاكي نسبة؛ وتسليمها النسبة مباشرة يعطيها سؤالًا واحدًا نظيفًا."},{"text":"الانحدار اللوجستي يتجاهل الـ features الجديدة بعد الثمانية الأولى.","why":"إنه يستخدم كل عمود يتلقّاه؛ لكن العمود الجديد لا يضيف شيئًا يُذكر فوق ما لديه أصلًا."},{"text":"الشجرة تقع في الـ overfitting على الـ feature الجديد.","why":"الـ overfitting سيضرّ نتائج التحقق، لا أن يحسّنها."},{"text":"النسبة تسرّب الهدف.","why":"الطلبات ومدة العضوية معروفتان كلتاهما في تاريخ اللقطة، فنسبتهما آمنة."}],"answer":0},{"q":"<p>لماذا نحسب الـ features داخل <code dir=\"ltr\">FunctionTransformer</code> في الـ pipeline بدل إضافة أعمدة إلى الـ DataFrame مسبقًا؟</p>","options":[{"text":"لأن <code dir=\"ltr\">FunctionTransformer</code> يجعل حساب الـ features أسرع.","why":"السرعة هي نفسها؛ فهو كود pandas نفسه في الحالتين."},{"text":"لأن خدمة الإنتاج تحسب حينها الـ features نفسها تمامًا من المدخلات الخام، دون كود منفصل يجب إبقاؤه متزامنًا.","why":"صحيح. كائن واحد يحمل منطق الـ features والنموذج معًا، فلا يمكن أن يبتعدا عن بعضهما."},{"text":"لأن الـ features المضافة إلى DataFrame لا يمكن أن تستخدمها نماذج scikit-learn.","why":"أي عمود رقمي في DataFrame يمكن استخدامه؛ والفائدة هي الاتّساق."}],"answer":1},{"q":"<p>العمود <code dir=\"ltr\">total_spent</code> ملتوٍ بشدة نحو اليمين (قلّة من العملاء أنفقوا الآلاف). أيّ تحويل يساعد النموذج الخطي عادةً؟</p>","options":[{"text":"تربيعه، لتمديد القيم الكبيرة أكثر.","why":"التربيع يزيد الالتواء سوءًا ويعطي العملاء المتطرّفين تأثيرًا أكبر."},{"text":"ترميزه بـ one-hot.","why":"هذا يعامل كل مبلغ مختلف كفئة مستقلة، فيرمي الترتيب ويفجّر عدد الأعمدة."},{"text":"<code dir=\"ltr\">np.log1p(total_spent)</code>، الذي يضغط القيم الكبيرة فيصبح لفرق 100 دولار وزن أكبر عند 50 دولارًا منه عند 3,000 دولار.","why":"صحيح. التحويلات اللوغاريتمية تروّض الالتواء وكثيرًا ما تجعل العلاقات أقرب إلى الخطية. والأشجار لا تحتاج إليها."}],"answer":2},{"q":"<p>feature جديد يرفع متوسط الـ AUC بالتحقق المتقاطع من 0.807 إلى 0.811، مع انحرافات معيارية للطيّات في حدود 0.035. ماذا تفعل؟</p>","options":[{"text":"احتفظ به؛ أي تحسّن يستحق الحصول عليه.","why":"لكل feature كلفة تشغيل (حساب، ومراقبة، وشرح)، ومكسب بهذا الحجم لا يمكن تمييزه عن الضجيج."},{"text":"أعلن عنه كتحسّن بمقدار 0.4 نقطة.","why":"هذا يبالغ في دليل غير موجود."},{"text":"اضبط النموذج من جديد حتى تكبر الفجوة.","why":"الضبط لتوسيع فجوة مليئة بالضجيج هو overfitting على طيّات التحقق."},{"text":"عامله على أنه لا تغيير؛ وأبقِ مجموعة الـ features الأبسط ما لم يكن للـ feature سبب آخر لوجوده.","why":"صحيح. المكسب الواقع ضمن التشتّت بوضوح ضجيج؛ والخيار الافتراضي ترك الـ feature خارجًا."}],"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":"أعطِ الشجرة معدّلًا تقسم عليه","prompt":"<p>ساعد شجرة قرار واحدة باثنين من الـ features المهندَسة.</p>\n<ol>\n<li>أكمل <code dir=\"ltr\">add_features(df)</code> بحيث تُعيد <strong>نسخة</strong> من <code dir=\"ltr\">df</code> مضافًا إليها <code dir=\"ltr\">orders_per_month</code> (<code dir=\"ltr\">orders / (tenure_days / 30.4)</code>) و<code dir=\"ltr\">tickets_per_order</code> (<code dir=\"ltr\">support_tickets / orders</code>). لا تعدّل الـ DataFrame المُمرَّر.</li>\n<li><code dir=\"ltr\">pipe</code>: <code dir=\"ltr\">FunctionTransformer(add_features)</code> متبوعًا بـ <code dir=\"ltr\">tree</code>.</li>\n<li><code dir=\"ltr\">plain_auc</code> و<code dir=\"ltr\">engineered_auc</code>: متوسط الـ ROC AUC بالتحقق المتقاطع على بيانات التدريب (باستخدام <code dir=\"ltr\">cv</code> المعطى) لـ <code dir=\"ltr\">tree</code> وحدها ولـ <code dir=\"ltr\">pipe</code>.</li>\n</ol>\n","hints":["ابدأ الدالة بـ <code dir=\"ltr\">df = df.copy()</code> كي تبقى بيانات المستدعي دون مساس.","<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> لكلٍّ منهما."],"explanation":"<p>متوسط الشجرة العادية نحو 0.72؛ ومع المعدّلين يصعد بعدة نقاط، ويعود الفضل في معظم ذلك إلى <code dir=\"ltr\">orders_per_month</code>، الذي يصبح السؤال الأول في الشجرة. ولأن الـ features تعيش داخل الـ pipeline، يحسبها التحقق المتقاطع وأي تنبؤ لاحق بالطريقة نفسها، ولأن كل قيمة تعتمد على صفّها وحده، فلا شيء يتسرّب بين الطيّات. أجرِ المقارنة نفسها مع الانحدار اللوجستي ولن ترى تغييرًا يُذكر: النماذج المختلفة تحتاج إلى مساعدة مختلفة.</p>\n","checkLabels":{"new-columns":"تضيف add_features العمودين الجديدين كليهما","values":"يحمل العمودان الجديدان القيم الصحيحة","no-mutation":"X_train نفسها لم تُعدَّل","pipeline":"pipe هو FunctionTransformer ← DecisionTreeClassifier","scores":"حُسبت نتيجتا التحقق المتقاطع، والشجرة المهندَسة تتفوّق على العادية"}},"words":657,"readMinutes":4}