{"id":"l-ml-4-4","courseId":"ml-crash-course","lang":"ar","kind":"lesson","summary":"تعرّف على الأنواع الثلاثة الشائعة لتسرّب البيانات (تسرّب الهدف، والمعالجة المسبقة قبل التقسيم، والصفوف أو الأزمنة المتداخلة) وامنعها باللقطات الزمنية والـ pipelines والـ splitter الصحيح.","html":"<p>لكل عالم بيانات متمرّس قصة عن النموذج الذي كان أجمل من أن يُصدَّق. دقة 99% في الـ notebook، وأداء رمي العملة في بيئة الإنتاج. والسبب يكاد يكون دائمًا واحدًا: أثناء التدريب أو التقييم أُتيحت للنموذج معلومات لن تكون لديه في الواقع. هذا هو <strong>تسرّب البيانات (data leakage)</strong>، وهو خطير لأنه لا يبدو كخطأ برمجي. بل يبدو كنجاح.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">تسرّب الهدف: features من المستقبل</h2>\n<p>جدول الانقطاع في Cartwheel <strong>لقطة زمنية (snapshot)</strong>: كل feature يصف العميل كما كان في 30 سبتمبر 2025، و<code dir=\"ltr\">churned</code> يسجّل ما حدث في الـ 90 يومًا التالية. هذا الخط الزمني هو العقد الذي يعيش النموذج وفقه. ففي وقت التنبؤ، لا تعرف إلا ما حدث قبل اللقطة.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 220\" role=\"img\" aria-labelledby=\"l-ml-4-4-t1\"><defs><marker id=\"arrow-l-ml-4-4-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-ml-4-4-t1\">خط زمني. تاريخ العميل حتى لقطة 30 سبتمبر هو المكان الذي يجوز أن تأتي منه الـ features. والأيام التسعون بعد اللقطة هي نافذة النتيجة التي تحدّد churned. والـ feature المحسوب من أحداث داخل نافذة النتيجة، مثل حداثة آخر طلب مقيسة في نهاية العام، تسرّب.</title>\n  <line class=\"d-line\" x1=\"30\" y1=\"110\" x2=\"650\" y2=\"110\"/>\n  <rect class=\"d-box-success\" x=\"30\" y=\"70\" width=\"360\" height=\"40\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"210\" y=\"95\" text-anchor=\"middle\">التاريخ: من هنا تأتي الـ features</text>\n  <rect class=\"d-box-warn\" x=\"390\" y=\"70\" width=\"260\" height=\"40\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"520\" y=\"95\" text-anchor=\"middle\">نافذة النتيجة (90 يومًا)</text>\n  <line class=\"d-line\" x1=\"390\" y1=\"50\" x2=\"390\" y2=\"140\"/>\n  <text class=\"d-label-strong\" x=\"390\" y=\"40\" text-anchor=\"middle\">30 سبتمبر: التنبؤ</text>\n  <text class=\"d-label-muted\" x=\"650\" y=\"160\" text-anchor=\"end\">29 ديسمبر: الانقطاع معروف</text>\n  <path class=\"d-arrow\" d=\"M560 180 L470 125\" marker-end=\"url(#arrow-l-ml-4-4-1)\"/>\n  <text class=\"d-label\" x=\"560\" y=\"200\" text-anchor=\"middle\">\"الحداثة في نهاية العام\" تسرّب</text>\n</svg><figcaption>يجب أن تأتي الـ features مما قبل تاريخ التنبؤ</figcaption></figure>\n<p>تخيّل الآن أن محلّلًا أعاد بناء الجدول في يناير وحسب <code dir=\"ltr\">days_since_last_order</code> حتى تاريخ التصدير بدل تاريخ اللقطة. بالنسبة إلى المنقطعين، الذين لم يطلبوا شيئًا في النافذة، تكون القيمة أكبر بـ 92 يومًا. وبالنسبة إلى العملاء الذين بقوا، تكون صغيرة، لأنهم طلبوا مؤخرًا. صار العمود الآن يرمّز الإجابة:</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.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</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\">rng </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> np.random.default_rng(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># Recency measured at export time (year end), not at the 30 September snapshot</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">churn[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"recency_at_export\"</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.where(churn[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churned\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">                                      churn[</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:#0550AE;--shiki-dark:#79C0FF\"> 92</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">                                      rng.integers(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">92</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">len</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(churn)))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">cv </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> StratifiedKFold(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n_splits</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">shuffle</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">True</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">random_state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">model </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline(StandardScaler(), LogisticRegression())</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cols </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [features, 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\">\"recency_at_export\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]]:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    auc </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cross_val_score(model, churn[cols], 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\">cv</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">cv, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">scoring</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"roc_auc\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).mean()</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:#0550AE;--shiki-dark:#79C0FF\">len</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cols)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> features: ROC AUC </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">auc</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=\"0\" hidden></div></div>\n<p>يقفز الـ ROC AUC من 0.81 إلى 0.999. لا شيء في الكود خاطئ؛ والتحقق المتقاطع مُنفَّذ كما ينبغي. التسرّب يكمن في طريقة بناء البيانات، ولهذا عليك أن تبحث عنه في تاريخ البيانات، لا في النموذج.</p>\n<p>تسرّبات الهدف تختبئ خلف أسماء بريئة: <code dir=\"ltr\">account_status</code> و<code dir=\"ltr\">last_contact_reason</code> و<code dir=\"ltr\">refund_amount</code> و<code dir=\"ltr\">total_orders</code> (المحسوب عند التصدير، بما فيه من طلبات مستقبلية). والاختبار لكل عمود سؤال واحد: <strong>هل كنت سأعرف هذه القيمة، تمامًا كما هي مخزّنة، في اللحظة التي أُجري فيها التنبؤ؟</strong></p>\n<h2 id=\"sec-2\" tabindex=\"-1\">المعالجة المسبقة قبل التقسيم</h2>\n<p>النوع الثاني من التسرّب يحدث في الكود. فأي خطوة تتعلّم من البيانات (توحيد المقاييس، والملء، والترميز، واختيار الـ features) يجب أن تتعلّم من صفوف التدريب وحدها. نفّذها على البيانات كلها أولًا وستكون صفوف التحقق قد شكّلت النموذج.</p>\n<p>مع توحيد المقاييس، يكون الضرر صغيرًا عادةً. أما مع اختيار الـ features فقد يكون هائلًا. إليك مجموعة بيانات من الضجيج الصرف: 200 صف، و2,000 عمود عشوائي، وlabels عشوائية. لا ينبغي لأي نموذج أن يتجاوز 50%.</p>\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\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.feature_selection </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> SelectKBest, f_classif</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</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>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">rng </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> np.random.default_rng(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> rng.normal(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">y </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> rng.integers(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">200</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:#1F2328;--shiki-dark:#E6EDF3\">X_selected </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> SelectKBest(f_classif, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">k</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\">).fit_transform(X, y)   </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># selection saw every label</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">leaky </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cross_val_score(LogisticRegression(), X_selected, y, </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).mean()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">pipe </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline(SelectKBest(f_classif, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">k</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\">), LogisticRegression())</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">honest </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cross_val_score(pipe, X, y, </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).mean()</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\">\"select then CV: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">leaky</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.3f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">   select inside CV: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">honest</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>الاختيار على الصفوف المئتين كلها يجد 20 عمودًا صادف أنها تتوافق مع labels كل صف، بما فيها الصفوف التي ستُستخدم لاحقًا للتحقق. ثم يؤكد التحقق المتقاطع نمطًا لا وجود له إلا لأنه اختير باستخدام تلك الصفوف نفسها: دقة 0.75 على ضجيج. وداخل pipeline، لا يرى الاختيار إلا طيّات التدريب، فتعود النتيجة إلى مستوى الصدفة.</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>“لم أفعل سوى توحيد مقاييس البيانات كلها، وهذا غير ضار”</span></p>\n<p>أحيانًا يكاد يكون كذلك. لكن العادة هي ما يهم: شكل الكود نفسه مع imputer أو target encoder أو أداة لاختيار الـ features ينتج تسرّبات تضخّم النتائج بـ 10 أو 20 نقطة. اجعل “كل خطوة متعلَّمة تعيش داخل الـ pipeline” قاعدة بلا استثناءات، ولن يُطرح السؤال أبدًا.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">صفوف يعرف بعضها بعضًا</h2>\n<p>النوع الثالث يأتي من علاقة الصفوف ببعضها. التقسيمات العشوائية تفترض أن الصفوف مستقلة. وحين لا تكون كذلك، تحتوي بيانات الاختبار على نسخ شبه مطابقة لصفوف التدريب.</p>\n<ul>\n<li><strong>الكيانات المتكرّرة.</strong> إذا كان في الجدول عدة صفوف لكل عميل أو مريض أو جهاز، فالتقسيم العشوائي يضع العميل نفسه في الجهتين، ويُقيَّم النموذج جزئيًا على أشخاص حفظهم. استخدم <code dir=\"ltr\">GroupKFold</code> (أو <code dir=\"ltr\">GroupShuffleSplit</code>) مع معرّف العميل كـ <code dir=\"ltr\">groups</code>.</li>\n<li><strong>الزمن.</strong> إذا كنت تتنبأ بالمستقبل، فالتقسيم العشوائي يتيح للنموذج أن يتعلّم من صفوف تأتي بعد الصفوف التي يُختبر عليها. استخدم <code dir=\"ltr\">TimeSeriesSplit</code>، أو تاريخًا فاصلًا بسيطًا: درّب على الماضي، وتحقّق على الفترات اللاحقة.</li>\n<li><strong>التكرارات.</strong> الصفوف المكرّرة حرفيًا الموزّعة بين التدريب والاختبار إجابات مجانية. احذفها قبل التقسيم.</li>\n</ul>\n<p>في جدول الانقطاع لدى Cartwheel صف واحد لكل عميل وتاريخ لقطة واحد، فالتقسيم العشوائي الطبقي مناسب. غيّر أيًّا من هذين ولن يعود كذلك.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>قائمة تحقق لصيد التسرّب</span></p>\n<p>كن متشكّكًا حين تقفز النتيجة بعد إضافة عمود واحد، أو حين يتفوّق نموذج على أفضل خبراء الشركة بفارق هائل، أو حين تضع الـ permutation importance شبه كل شيء على feature واحد باسم غامض. عندها تتبّع ذلك العمود إلى الاستعلام الذي أنشأه وقارن طابعه الزمني بتاريخ التنبؤ.</p>\n</aside>\n<p>مع إغلاق منافذ التسرّب، يمكنك الوثوق بالتحقق المتقاطع للحكم على الأفكار الجديدة، وأكثر الأفكار الجديدة إنتاجًا عادةً هي features أفضل. يبني الدرس القادم بعضها من المعرفة بمجال العمل ويختبر ما إذا كانت تستحق مكانها.</p>\n","toc":[{"id":"sec-1","text":"تسرّب الهدف: features من المستقبل","level":2},{"id":"sec-2","text":"المعالجة المسبقة قبل التقسيم","level":2},{"id":"sec-3","text":"صفوف يعرف بعضها بعضًا","level":2}],"takeaways":["تسرّب البيانات (data leakage) هو أي معلومة متاحة أثناء التدريب أو التقييم لن تكون متاحة حين يتنبأ النموذج فعليًا.","يأتي تسرّب الهدف من features سُجّلت بعد لحظة التنبؤ؛ فاسأل عن كل عمود: هل كنت سأعرف هذه القيمة وقت التنبؤ؟","تدريب scaler أو imputer أو أداة لاختيار الـ features قبل التقسيم يتيح لصفوف التحقق أن تشكّل النموذج؛ فضع كل خطوة متعلَّمة داخل pipeline.","حين تنتمي الصفوف إلى العميل نفسه أو يتبع بعضها بعضًا زمنيًا، استخدم <code dir=\"ltr\">GroupKFold</code> أو <code dir=\"ltr\">TimeSeriesSplit</code> بدل التقسيم العشوائي.","النتيجة التي تبدو أجمل من أن تُصدَّق، أو feature واحد يهيمن على الـ permutation importance، سبب للبحث عن تسرّب قبل الاحتفال."],"further":[{"title":"Common pitfalls: data leakage","url":"https://scikit-learn.org/stable/common_pitfalls.html#data-leakage"},{"title":"Cross-validation iterators for grouped data","url":"https://scikit-learn.org/stable/modules/cross_validation.html#group-k-fold"},{"title":"TimeSeriesSplit","url":"https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.TimeSeriesSplit.html"}],"quiz":[{"q":"<p>أضاف زميلك <code dir=\"ltr\">refund_issued_in_q4</code> إلى features الانقطاع فقفز الـ ROC AUC من 0.81 إلى 0.97. والتنبؤ يتم في 30 سبتمبر. ما المشكلة؟</p>","options":[{"text":"الـ feature يصف أحداثًا بعد تاريخ التنبؤ، فلن يكون موجودًا حين يعمل النموذج؛ إنه تسرّب للهدف.","why":"صحيح. استرداد الأموال في الربع الرابع يحدث خلال نافذة النتيجة، فالنموذج يقرأ الإجابة جزئيًا."},{"text":"الـ feature ثنائي، والـ features الثنائية تضخّم الـ AUC دائمًا.","why":"كثير من الـ features المشروعة ثنائية (<code dir=\"ltr\">used_coupon</code>). المشكلة في التوقيت، لا في النوع."},{"text":"لا شيء؛ فاسترداد الأموال محرّك معروف للانقطاع.","why":"قد يكون كذلك فعلًا، لكن النموذج لا يستطيع أن يستخدم إلا ما هو معروف وقت التنبؤ."},{"text":"النموذج يعاني من الـ overfitting ويحتاج إلى <code dir=\"ltr\">C</code> أصغر.","why":"العقوبة الأقوى لن تمنع النموذج من استخدام feature يرمّز النتيجة."}],"answer":0},{"q":"<p>على 2,000 عمود من الضجيج الصرف، يعطي اختيار أفضل 20 عمودًا على البيانات كلها ثم تطبيق التحقق المتقاطع دقة 0.75. وداخل pipeline يعطي 0.48. لماذا؟</p>","options":[{"text":"نسخة الـ pipeline تستخدم صفوف تدريب أقل، فهي أضعف.","why":"كلتاهما تستخدم الطيّات نفسها؛ والفرق في المكان الذي يحدث فيه الاختيار."},{"text":"الاختيار على كل الصفوف التقط أعمدة صادف أنها ترتبط بـ labels صفوف التحقق أيضًا، فكأن تلك الصفوف رُئيت أثناء التدريب.","why":"صحيح. داخل الـ pipeline لا يرى الاختيار إلا صفوف تدريب كل طيّة، فلا يستطيع الضجيج أن يتوافق مع labels التحقق."},{"text":"SelectKBest عشوائي، فاختار التشغيلان أعمدة مختلفة.","why":"SelectKBest حتمي؛ والفرق في الصفوف التي نظر إليها."}],"answer":1},{"q":"<p>في سجلّ الطلبات عدة صفوف لكل عميل، وتريد التنبؤ على مستوى الطلب. أيّ splitter يتجنّب التسرّب بين الطيّات؟</p>","options":[{"text":"<code dir=\"ltr\">StratifiedKFold(shuffle=True)</code>، لأنه يوازن الفئات.","why":"توازن الفئات لا يمنع وقوع طلبات العميل نفسه في طيّات التدريب والتحقق معًا."},{"text":"<code dir=\"ltr\">KFold(shuffle=False)</code>، لتحافظ الصفوف على ترتيبها.","why":"الطيّات غير المخلوطة تظل تقسم صفوف العميل بين الطيّات حين لا تكون متجاورة."},{"text":"<code dir=\"ltr\">GroupKFold</code> مع معرّف العميل كمجموعة، فتبقى صفوف كل عميل في طيّة واحدة.","why":"صحيح. عندها يُقيَّم النموذج دائمًا على عملاء لم يرَهم قط."}],"answer":2},{"q":"<p>أيّ إشارة توحي بقوة بوجود تسرّب؟</p>","options":[{"text":"الـ accuracy بالتحقق المتقاطع أعلى من قاعدة خط الأساس ببضع نقاط.","why":"هكذا يبدو النموذج المتواضع الأمين."},{"text":"تتفاوت نتائج الطيّات بخمس نقاط.","why":"التفاوت يعكس صغر الطيّات والضجيج، لا التسرّب."},{"text":"دقة التدريب أعلى من دقة الاختبار.","why":"هذا طبيعي في معظم النماذج إن لم يكن كلها."},{"text":"feature غير مألوف يحمل الـ permutation importance كلها أو يكاد، والنتيجة قريبة من الكمال.","why":"صحيح. المسائل الحقيقية نادرًا ما يكون فيها متنبئ واحد شبه مثالي؛ وحين يظهر أحدها، تحقّق من متى وكيف سُجّل."}],"answer":3}],"exercise":{"kind":"spot-bug","language":"python","code":"import pandas as pd\nfrom sklearn.impute import SimpleImputer\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.model_selection import cross_val_score, train_test_split\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.preprocessing import StandardScaler\n\nchurn = pd.read_csv(\"churn_export_january.csv\")\nchurn[\"lifetime_orders\"] = churn[\"orders\"] + churn[\"orders_oct_to_dec\"]\nX = churn.drop(columns=[\"customer_id\", \"churned\", \"country\", \"segment\", \"orders_oct_to_dec\"])\nX = SimpleImputer(strategy=\"median\").fit_transform(X)\ny = churn[\"churned\"]\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)\nmodel = make_pipeline(StandardScaler(), LogisticRegression())\nprint(cross_val_score(model, X_train, y_train, cv=5, scoring=\"roc_auc\").mean())","bugLines":[9,11],"title":"اعثر على التسرّبات","prompt":"<p>يعرض notebook زميلك للتنبؤ بانقطاع العملاء قيمة ROC AUC بالتحقق المتقاطع تبلغ 0.98. يتنبأ النموذج في 30 سبتمبر؛ وملف تصدير يناير يتضمّن أيضًا <code dir=\"ltr\">orders_oct_to_dec</code>، أي الطلبات المقدَّمة بين أكتوبر وديسمبر. اختر كل سطر يسرّب معلومات لن تكون لدى النموذج وقت التنبؤ، أو يتيح لصفوف التحقق التأثير في التدريب.</p>\n","hints":["افحص كل feature مقابل الخط الزمني: ما الذي كنت ستعرفه في 30 سبتمبر؟","ابحث عن أي شيء يستدعي <code dir=\"ltr\">fit</code> أو <code dir=\"ltr\">fit_transform</code> على البيانات قبل التقسيم.","هناك سطران فيهما تسرّب: واحد في البيانات، وواحد في الكود."],"explanation":"<p><strong>السطر 9</strong> يبني feature من <code dir=\"ltr\">orders_oct_to_dec</code>، الذي يغطي نافذة النتيجة البالغة 90 يومًا: العملاء المنقطعون لديهم صفر طلبات هناك بحكم التعريف، فيرمّز <code dir=\"ltr\">lifetime_orders</code> الـ label جزئيًا. صحيح أن السطر 10 يحذف العمود الأصلي، لكن بعد فوات الأوان: قيمه باقية داخل <code dir=\"ltr\">lifetime_orders</code>. احذف الـ feature المشتق أيضًا، أو احسب أي أعداد طلبات حتى تاريخ اللقطة فقط. <strong>السطر 11</strong> يدرّب الـ imputer على كل الصفوف قبل التقسيم، فتدخل صفوف الاختبار والتحقق في حساب الوسيطات. انقله إلى داخل الـ pipeline بدلًا من ذلك:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><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:#1F2328;--shiki-dark:#E6EDF3\">model </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline(SimpleImputer(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">strategy</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"median\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), StandardScaler(), LogisticRegression())</span></span></code></pre></div>\n<p>والباقي سليم: الـ scaler داخل الـ pipeline أصلًا، والتقسيم طبقي، والتحقق المتقاطع يعمل على جزء التدريب وحده.</p>\n","checkLabels":{}},"words":653,"readMinutes":4}