{"id":"l-ml-3-5","courseId":"ml-crash-course","lang":"ar","kind":"lesson","summary":"حرّك عتبة القرار لتبادل الـ precision بالـ recall، واخترها من تكاليف العمل باستخدام TunedThresholdClassifierCV، وتعامل مع الفئات النادرة بأوزان الفئات والمقاييس الصحيحة.","html":"<p>ظلّت <code dir=\"ltr\">predict</code> تتخذ قرارًا نيابةً عنك بصمت: صنّف العميل منقطعًا حين يكون الاحتمال 0.5 على الأقل. هذا الحدّ الفاصل يفترض أن تفويت منقطع وإهدار عرض لهما الكلفة نفسها. وهذا لا يكاد يحدث أبدًا. اختيار العتبة (threshold) عن قصد من أرخص التحسينات وأعلاها قيمة التي يمكنك إجراؤها على أي مصنِّف، ولا يتطلّب إعادة تدريب أي شيء.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">العتبة مقبض تديره</h2>\n<p>الاحتمالات تأتي من النموذج؛ أما العتبة فهي سياستك أنت. حرّكها وراقب الـ precision والـ recall يتجاذبان.</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\"> 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\"> precision_score, recall_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 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()).fit(X_train, y_train)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">prob </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> model.predict_proba(X_test)[:, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> t </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.5</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.6</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.7</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.8</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    flagged </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (prob </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">>=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> t).astype(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">int</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\">\"threshold </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">t</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.1f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">: flags </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">flagged.sum()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:2}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  precision </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">precision_score(y_test, flagged)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.2f}</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\">\"  recall </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">recall_score(y_test, flagged)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.2f}</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<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 270\" role=\"img\" aria-labelledby=\"l-ml-3-5-t1\"><defs><marker id=\"arrow-l-ml-3-5-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-3-5-t1\">مخطط خطي على العتبات من 0.3 إلى 0.8. ترتفع الـ precision بلطف من 0.69 إلى 0.86. وتنخفض الـ recall بحدّة من 0.93 إلى 0.33. العتبة الافتراضية 0.5 مُعلَّمة بخط عمودي متقطع.</title>\n  <line class=\"d-line\" x1=\"80\" y1=\"220\" x2=\"620\" y2=\"220\"/>\n  <line class=\"d-line\" x1=\"80\" y1=\"20\" x2=\"80\" y2=\"220\"/>\n  <text class=\"d-label-muted\" x=\"70\" y=\"24\" text-anchor=\"end\">1.0</text>\n  <text class=\"d-label-muted\" x=\"70\" y=\"124\" text-anchor=\"end\">0.5</text>\n  <text class=\"d-label-muted\" x=\"70\" y=\"224\" text-anchor=\"end\">0</text>\n  <text class=\"d-label-muted\" x=\"100\" y=\"242\" text-anchor=\"middle\">0.3</text>\n  <text class=\"d-label-muted\" x=\"300\" y=\"242\" text-anchor=\"middle\">0.5</text>\n  <text class=\"d-label-muted\" x=\"600\" y=\"242\" text-anchor=\"middle\">0.8</text>\n  <text class=\"d-label-muted\" x=\"350\" y=\"264\" text-anchor=\"middle\">العتبة</text>\n  <line class=\"d-dashed d-line\" x1=\"300\" y1=\"20\" x2=\"300\" y2=\"220\"/>\n  <path class=\"d-arrow\" d=\"M100 82 L200 76 L300 62 L400 56 L500 58 L600 48\" fill=\"none\"/>\n  <path class=\"d-line\" d=\"M100 34 L200 60 L300 86 L400 100 L500 130 L600 154\" fill=\"none\"/>\n  <circle class=\"d-dot\" cx=\"300\" cy=\"62\" r=\"5\"/>\n  <circle class=\"d-dot\" cx=\"300\" cy=\"86\" r=\"5\"/>\n  <text class=\"d-label\" x=\"610\" y=\"44\">precision</text>\n  <text class=\"d-label\" x=\"610\" y=\"160\">recall</text>\n</svg><figcaption>الـ precision والـ recall مع تحرّك العتبة (بيانات اختبار الانقطاع)</figcaption></figure>\n<p>عند 0.3 يُعلِّم النموذج 74 عميلًا ويلتقط 93% من المنقطعين، لكن 31% من العلامات خاطئة. وعند 0.8 يُعلِّم 21، و86% منها صحيحة، لكنه لا يلتقط إلا ثلث المنقطعين. لا طرف “أفضل” من الآخر. النقطة الصحيحة تعتمد على المال.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">دع التكاليف تختار العتبة</h2>\n<p>أرقام Cartwheel: عرض الاسترجاع يكلّف 20 دولارًا بين خصم ومعالجة. والتواصل مع منقطع حقيقي يُنقذه في 30% من الحالات، والعميل المُنقذ يساوي 100 دولار من هامش الربح، فكل منقطع نتواصل معه يساوي في المتوسط <code dir=\"ltr\">0.3 * 100 - 20 = +$10</code>. أما التواصل مع عميل وفيّ فيُهدر الـ 20 دولارًا. والربح هو <code dir=\"ltr\">10 * TP - 20 * FP</code>.</p>\n<p>لو كانت الاحتمالات معايَرة بإتقان، لاستطعت حلّ معادلة نقطة التعادل: تواصَل حين يكون <code dir=\"ltr\">p * 10 &gt; (1 - p) * 20</code>، أي حين <code dir=\"ltr\">p &gt; 2/3</code>. لكن الاحتمالات الحقيقية نادرًا ما تكون معايَرة بهذه الجودة، فالنهج الأكثر موثوقية هو البحث عن العتبة التي تعظّم الربح، باستخدام التحقق المتقاطع على بيانات التدريب. وهذا بالضبط ما يفعله <code dir=\"ltr\">TunedThresholdClassifierCV</code>.</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\">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\"> make_scorer</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\"> TunedThresholdClassifierCV, 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:#CF222E;--shiki-dark:#FF7B72\">def</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> profit</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(y_true, y_pred):</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    y_true, y_pred </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> np.asarray(y_true), np.asarray(y_pred)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    tp </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ((y_pred </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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (y_true </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\">)).sum()</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    fp </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ((y_pred </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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (y_true </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).sum()</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 10</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tp </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 20</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> fp</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">base </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:#1F2328;--shiki-dark:#E6EDF3\">tuned </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> TunedThresholdClassifierCV(base, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">scoring</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">make_scorer(profit), </span><span style=\"color:#953800;--shiki-dark:#FFA657\">cv</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\">).fit(X_train, y_train)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"chosen threshold:\"</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\">(tuned.best_threshold_, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</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\">default </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> base.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 profit, contact everyone: \"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, profit(y_test, np.ones(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">len</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(y_test))))</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 profit, threshold 0.5:    \"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, profit(y_test, default.predict(X_test)))</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 profit, tuned threshold:  \"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, profit(y_test, tuned.predict(X_test)))</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p>التواصل مع عملاء الاختبار المئة كلهم يخسر 350 دولارًا. العتبة الافتراضية تربح 170 دولارًا؛ والمضبوطة، وهي نحو 0.58، تربح 180 دولارًا مع التواصل مع عدد أقل من الناس. أما القيمة النظرية 0.67 فكانت ستربح أقل، لأن احتمالات النموذج منحرفة قليلًا: وهذا تذكير بأن تثق بالنتائج المقيسة أكثر من المعادلات الأنيقة. تحوّل <code dir=\"ltr\">make_scorer</code> أي دالة <code dir=\"ltr\">(y_true, y_pred)</code> إلى شيء تستطيع أدوات الضبط في scikit-learn تعظيمه.</p>\n<p>يكون الاحتمال <strong>معايَرًا (calibrated)</strong> حين ينقطع فعلًا نحو 70% من كل العملاء الذين نالوا درجة 0.7. الانحدار اللوجستي يقترب من ذلك غالبًا؛ والأشجار المعزّزة والغابات تكون أبعد غالبًا. وحين تحتاج إلى أن تكون الاحتمالات نفسها صحيحة (لتقدير الإيرادات المتوقعة مثلًا لا للترتيب)، يستطيع <code dir=\"ltr\">CalibratedClassifierCV</code> في scikit-learn إعادة معايرتها. أما لاختيار عتبة، فالضبط المباشر على المقياس التجاري يتجاوز هذا السؤال.</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>جدول العتبات أعلاه حُسب على بيانات الاختبار ليبيّن المفاضلة. واختيار عتبة منه ونشر ربح الاختبار ذاك سيكون متفائلًا، وهي مشكلة اختلاس النظر نفسها التي تقع مع أي hyperparameter. يختار <code dir=\"ltr\">TunedThresholdClassifierCV</code> باستخدام طيّات (folds) التحقق المتقاطع من بيانات التدريب وحدها؛ وتُستخدم بيانات الاختبار مرة واحدة، للإبلاغ.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">حين تكون الفئة الإيجابية نادرة</h2>\n<p>المنقطعون 55% من العملاء، فكلتا الفئتين ممثّلة جيدًا. لكن كثيرًا من المسائل الحقيقية ليست كذلك: الاحتيال، والعيوب، والأمراض النادرة. خذ بيانات الأرقام (digits) واسأل “هل هذا 9؟”. 10% فقط من الصور كذلك، فالنموذج الذي يجيب “لا” دائمًا يحقق accuracy بنسبة 90%.</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\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn.datasets </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> load_digits</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\"> average_precision_score, precision_score, recall_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\">X, digit </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> load_digits(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">return_X_y</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">True</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\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\"> (digit </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 9</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).astype(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">int</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">X_train, X_test, y_train, y_test </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> train_test_split(X, y, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">test_size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.25</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">random_state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">42</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">stratify</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">y)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"share of 9s:\"</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\">(y.mean(), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> weights </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">None</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"balanced\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    m </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline(StandardScaler(), LogisticRegression(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">class_weight</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">weights, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">max_iter</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">))</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    m.fit(X_train, y_train)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    pred, prob </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> m.predict(X_test), m.predict_proba(X_test)[:, </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:#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\">\"class_weight=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">weights</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!s:9}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> accuracy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">m.score(X_test, y_test)</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\">\"precision </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">precision_score(y_test, pred)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.2f}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  recall </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">recall_score(y_test, pred)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:.2f}</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\">\"avg precision </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">average_precision_score(y_test, prob)</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=\"2\" hidden></div></div>\n<p>لا تكاد الـ accuracy تتحرك، من 0.98 إلى 0.97، ولا تخبرك بشيء مفيد. أما الـ precision والـ recall فتحكيان القصة الحقيقية. يزن <code dir=\"ltr\">class_weight=&quot;balanced&quot;</code> كل فئة بعكس تكرارها أثناء التدريب، فيكلّف الرقم 9 الفائت نحو تسعة أضعاف الإنذار الكاذب. ترتفع الـ recall من 0.82 إلى 0.91، وتنخفض الـ precision من 0.97 إلى 0.82. وتلخّص <strong>الـ average precision</strong> (المساحة تحت منحنى precision-recall) جودة الترتيب مع تركيز على الفئة النادرة؛ وهي عادةً أغنى بالمعلومات من الـ ROC AUC حين تكون الحالات الإيجابية شحيحة.</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>كلاهما يحرّك المفاضلة نفسها. أوزان الفئات تغيّر ما يتعلّمه النموذج؛ والعتبة تغيّر كيف تتصرّف بناءً على درجاته. ابدأ بالعتبة، لأنها أرخص، وقابلة للتراجع، ومرتبطة مباشرة بالتكاليف. وأضف أوزان الفئات حين تكون الفئة النادرة نادرة إلى حدّ أن النموذج لا يكاد يتعلّمها.</p>\n</aside>\n<p>ظللت تقارن النماذج والعتبات على بيانات اختبار واحدة من 100 عميل، وتتحفّظ على كل فجوة بأنها “ربما ضجيج”. يستبدل القسم 4 هذا التقسيم الواحد بالتحقق المتقاطع، فتستطيع أخيرًا أن تقول كم يجب أن يكون الفرق كبيرًا قبل أن يكون حقيقيًا.</p>\n","toc":[{"id":"sec-1","text":"العتبة مقبض تديره","level":2},{"id":"sec-2","text":"دع التكاليف تختار العتبة","level":2},{"id":"sec-3","text":"حين تكون الفئة الإيجابية نادرة","level":2}],"takeaways":["تطبّق <code dir=\"ltr\">predict</code> عتبة 0.5 على الاحتمال؛ ويمكنك تطبيق أي عتبة بنفسك بـ <code dir=\"ltr\">predict_proba(X)[:, 1] &gt;= t</code>.","رفع العتبة يُعلِّم حالات أقل، فيرفع الـ precision عادةً ويخفض الـ recall؛ وخفضها يفعل العكس.","اختر العتبة من كلفة كل خطأ، على بيانات التدريب عبر التحقق المتقاطع، مثلًا باستخدام <code dir=\"ltr\">TunedThresholdClassifierCV</code> ودالة تقييم مخصّصة.","حين تكون الفئة الإيجابية نادرة، تكون الـ accuracy مضلِّلة؛ فاعرض بدلًا منها الـ precision والـ recall والـ average precision.","يجعل <code dir=\"ltr\">class_weight=&quot;balanced&quot;</code> أخطاء الفئة النادرة تُحسب أكثر أثناء التدريب، فيرفع الـ recall على حساب شيء من الـ precision."],"further":[{"title":"Tuning the decision threshold for class prediction","url":"https://scikit-learn.org/stable/modules/classification_threshold.html"},{"title":"TunedThresholdClassifierCV","url":"https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.TunedThresholdClassifierCV.html"},{"title":"precision_recall_curve","url":"https://scikit-learn.org/stable/modules/generated/sklearn.metrics.precision_recall_curve.html"}],"quiz":[{"q":"<p>خفضت عتبة الانقطاع من 0.5 إلى 0.3. ماذا تتوقع؟</p>","options":[{"text":"ترتفع الـ precision والـ recall معًا، لأن النموذج يرى منقطعين أكثر.","why":"النموذج نفسه لا يتغيّر. تعليم عملاء أكثر يلتقط منقطعين أكثر، لكنه يلتقط باقين أكثر أيضًا."},{"text":"لا بد أن ترتفع الـ accuracy، لأن عددًا أكبر من المنقطعين يُلتقط.","why":"قد تتحرك الـ accuracy في أي اتجاه؛ فقد تفوق الإيجابياتُ الكاذبة الإضافية الإيجابياتِ الصحيحة الإضافية."},{"text":"لا يتغيّر شيء، لأن العتبة لا تؤثر إلا في <code dir=\"ltr\">predict_proba</code>.","why":"العكس هو الصحيح. الاحتمالات تبقى كما هي؛ والـ labels المشتقة منها هي التي تتغيّر."},{"text":"يُعلَّم عملاء أكثر؛ فترتفع الـ recall وتنخفض الـ precision عادةً.","why":"صحيح. على بيانات اختبار الانقطاع، ترتفع الـ recall من 0.67 إلى 0.93 بينما تنخفض الـ precision من 0.79 إلى 0.69."}],"answer":3},{"q":"<p>إنقاذ عميل منقطع يساوي +10 دولارات صافية، والتواصل مع عميل وفيّ يكلّف 20 دولارًا. إذا كانت الاحتمالات معايَرة جيدًا، فعند أيّ احتمال انقطاع يصبح التواصل مجديًا؟</p>","options":[{"text":"فوق نحو 0.67، حيث <code dir=\"ltr\">p * 10 = (1 - p) * 20</code>.","why":"صحيح. دون الثلثين، تفوق الخسارة المتوقعة على الباقين المكسبَ المتوقع من المنقطعين."},{"text":"فوق 0.5، العتبة الافتراضية.","why":"تفترض القيمة الافتراضية أن الخطأين متساويان في الكلفة، وهذا غير صحيح هنا."},{"text":"فوق نحو 0.33، لأن قيمة المنقطع نصف كلفة التواصل المهدر.","why":"هذا يعكس المنطق؛ فالإيجابيات الكاذبة المكلفة تدفع العتبة إلى الأعلى، لا إلى الأسفل."}],"answer":0},{"q":"<p>في بيانات الأرقام (digits)، 10% من الصور تمثّل الرقم 9. يحقق نموذج accuracy بنسبة 98%. ماذا يجب أن تعرض بجانبها؟</p>","options":[{"text":"لا شيء؛ 98% ممتازة لأي مسألة.","why":"التنبؤ بـ “ليس 9” لكل شيء يحقق 90% أصلًا، فالـ 98% تحتاج إلى سياق."},{"text":"الـ precision والـ recall لفئة الرقم 9، إضافة إلى خط أساس الفئة الأكثر شيوعًا البالغ 90%.","why":"صحيح. هذه تبيّن كم 9 يُعثر عليه وكم إنذارًا كاذبًا، وهو ما تُخفيه الـ accuracy."},{"text":"دقة التدريب، لتُظهر أنه لا يوجد overfitting.","why":"مفيدة بشكل عام، لكنها لا تقول شيئًا عن مدى إحسان التعامل مع الفئة النادرة."}],"answer":1},{"q":"<p>لماذا يُعدّ اختيار العتبة التي تعظّم الربح على بيانات الاختبار خطأً؟</p>","options":[{"text":"لأن العتبات لا يمكن اختيارها إلا قبل التدريب.","why":"تُطبَّق العتبات بعد التدريب؛ واختيار إحداها لاحقًا أمر طبيعي. المشكلة في البيانات التي تختارها عليها."},{"text":"لأن الربح ليس مقياسًا صالحًا في تعلّم الآلة.","why":"أي دالة في التنبؤات والنتائج يمكن أن تكون مقياسًا؛ ومقياس العمل كثيرًا ما يكون الأفضل."},{"text":"لأن العتبة تصبح hyperparameter مضبوطًا، فيكون الربح الذي تُبلغ عنه على الاختبار متفائلًا.","why":"صحيح. اخترها بالتحقق المتقاطع على بيانات التدريب، ثم قِس مرة واحدة على بيانات الاختبار."}],"answer":2}],"exercise":{"kind":"python","files":["churn.csv"],"starter":"import numpy as np\nimport pandas as pd\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.metrics import make_scorer\nfrom sklearn.model_selection import TunedThresholdClassifierCV, train_test_split\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.preprocessing import StandardScaler\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\"])\n\n# 1. Email campaign economics: each contacted churner is worth +16, each contacted stayer costs -4.\ndef profit(y_true, y_pred):\n    return 0\n\n# 2. Tune the threshold of a StandardScaler + LogisticRegression pipeline for profit (cv=5)\ntuned = None\n\n# 3. Test-set profit at the default 0.5 threshold and at the tuned threshold\nprofit_default = None\nprofit_tuned = None\n","solution":"import numpy as np\nimport pandas as pd\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.metrics import make_scorer\nfrom sklearn.model_selection import TunedThresholdClassifierCV, train_test_split\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.preprocessing import StandardScaler\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\"])\n\ndef profit(y_true, y_pred):\n    y_true, y_pred = np.asarray(y_true), np.asarray(y_pred)\n    tp = ((y_pred == 1) & (y_true == 1)).sum()\n    fp = ((y_pred == 1) & (y_true == 0)).sum()\n    return 16 * tp - 4 * fp\n\nbase = make_pipeline(StandardScaler(), LogisticRegression())\ntuned = TunedThresholdClassifierCV(base, scoring=make_scorer(profit), cv=5).fit(X_train, y_train)\n\ndefault = make_pipeline(StandardScaler(), LogisticRegression()).fit(X_train, y_train)\nprofit_default = profit(y_test, default.predict(X_test))\nprofit_tuned = profit(y_test, tuned.predict(X_test))\nprint(round(tuned.best_threshold_, 2), profit_default, profit_tuned)\n","checks":[{"id":"profit-fn","test":"int(profit([1, 1, 0, 0, 1], [1, 0, 1, 1, 1])) == 16 * 2 - 4 * 2 and int(profit([0, 1], [0, 0])) == 0"},{"id":"tuned","test":"tuned is not None and hasattr(tuned, 'best_threshold_') and float(tuned.best_threshold_) < 0.5"},{"id":"default-profit","test":"profit_default is not None and int(profit_default) == int(profit(y_test, (__import__('sklearn.pipeline', fromlist=['x']).make_pipeline(__import__('sklearn.preprocessing', fromlist=['x']).StandardScaler(), __import__('sklearn.linear_model', fromlist=['x']).LogisticRegression()).fit(X_train, y_train).predict_proba(X_test)[:, 1] >= 0.5).astype(int)))"},{"id":"tuned-profit","test":"profit_tuned is not None and int(profit_tuned) == int(profit(y_test, tuned.predict(X_test))) and int(profit_tuned) > int(profit_default)"}],"title":"اضبط العتبة لبريد إلكتروني رخيص","prompt":"<p>انتقل فريق التسويق من المكالمات الهاتفية إلى بريد إلكتروني رخيص. الآن كل منقطع نتواصل معه يساوي في المتوسط <strong>+16</strong>، وكل عميل وفيّ نتواصل معه يكلّف <strong>4</strong>.</p>\n<ol>\n<li>اكتب <code dir=\"ltr\">profit(y_true, y_pred)</code> التي تُعيد <code dir=\"ltr\">16 * TP - 4 * FP</code>.</li>\n<li><code dir=\"ltr\">tuned</code>: نموذج <code dir=\"ltr\">TunedThresholdClassifierCV</code> يغلّف pipeline من <code dir=\"ltr\">StandardScaler</code> + <code dir=\"ltr\">LogisticRegression</code>، ويُقيَّم بـ <code dir=\"ltr\">make_scorer(profit)</code> و<code dir=\"ltr\">cv=5</code>، ومدرَّب على بيانات التدريب.</li>\n<li><code dir=\"ltr\">profit_default</code>: ربح الـ pipeline نفسه على بيانات الاختبار مع العتبة الافتراضية 0.5. و<code dir=\"ltr\">profit_tuned</code>: ربح <code dir=\"ltr\">tuned</code> على بيانات الاختبار.</li>\n</ol>\n","hints":["حوّل المدخلين بـ <code dir=\"ltr\">np.asarray</code>، ثم عُدّ <code dir=\"ltr\">(y_pred == 1) &amp; (y_true == 1)</code> لـ TP و<code dir=\"ltr\">(y_pred == 1) &amp; (y_true == 0)</code> لـ FP.","<code dir=\"ltr\">TunedThresholdClassifierCV(base, scoring=make_scorer(profit), cv=5).fit(X_train, y_train)</code>","للقيمة الافتراضية، درّب pipeline جديدًا ومرّر <code dir=\"ltr\">predict(X_test)</code> الخاصة به إلى <code dir=\"ltr\">profit</code>."],"explanation":"<p>حين يكون التواصل رخيصًا والمنقطع المُنقذ ثمينًا، يُجدي أن ترمي شبكة أوسع، فتنخفض العتبة المضبوطة إلى ما دون 0.5 بكثير (نقطة التعادل مع احتمالات معايَرة ستكون 0.2). يتواصل النموذج المضبوط مع عملاء أكثر ويربح أكثر على بيانات الاختبار. النموذج نفسه والاحتمالات نفسها: لم تتغيّر إلا السياسة، المختارة من الحسابات الاقتصادية على بيانات التدريب.</p>\n","checkLabels":{"profit-fn":"profit تُعيد 16·TP − 4·FP","tuned":"tuned مدرَّب وعتبته أقل من 0.5","default-profit":"profit_default هو ربح الاختبار عند العتبة 0.5","tuned-profit":"profit_tuned هو ربح النموذج المضبوط على الاختبار، وهو يتفوّق على الافتراضي"}},"words":627,"readMinutes":4}