{"id":"l-ml-5-5","courseId":"ml-crash-course","lang":"ar","kind":"lesson","summary":"احفظ الـ pipeline البطل باستخدام joblib، وغلّفه في دالة تنبؤ تتحقق من مدخلاتها، وقدّمه كمهمة دفعية ليلية أو كـ API، وقِس ما إذا كان يغيّر نتائج العمل.","html":"<p>النموذج الحبيس في notebook لا يساعد أحدًا. وكي يغيّر ما يفعله Cartwheel، يجب أن يعمل الـ pipeline البطل على بيانات عملاء جديدة، وفق جدول زمني أو عند الطلب، وأن تصل درجاته إلى الأشخاص الذين يرسلون عروض الاسترجاع. يغطي هذا الدرس الميل الأخير: حفظ النموذج، وحراسة مدخلاته، والاختيار بين مهمة دفعية وAPI، وإثبات أنه يعمل.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">احفظ الـ pipeline، لا النموذج وحده</h2>\n<p>الشيء الذي يُطلق هو الـ pipeline المدرَّب كاملًا. فمتوسطات الـ scaler وانحرافاته المعيارية جزء من النموذج بقدر المعاملات؛ ودونها ستوحّد بيئة الإنتاج مقاييس العملاء الجدد بشكل مختلف، وستكون كل درجة منحرفة.</p>\n<p>يغطي دليل حفظ النماذج في scikit-learn عدة صيغ (pickle وjoblib وskops وONNX)؛ و<code dir=\"ltr\">joblib</code> هو الأكثر شيوعًا حين تتولّى Python الحفظ والتحميل معًا. احفظ حزمة صغيرة فيها ما سيحتاجه أحدهم لاحقًا: الـ pipeline، وقائمة الـ features بالضبط، ووسم الإصدار، وإصدار المكتبة الذي دُرِّب به.</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\"> os</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tempfile</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> joblib</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\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sklearn</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.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\">champion </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> make_pipeline(StandardScaler(), LogisticRegression()).fit(churn[features], 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\">bundle </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"model\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: champion, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"features\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: features,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">          \"version\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churn-2025-09-30.1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"sklearn_version\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: sklearn.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">__version__</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\">with</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tempfile.TemporaryDirectory() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> folder:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    path </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> os.path.join(folder, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churn_model.joblib\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    joblib.dump(bundle, path)</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\">\"saved\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, os.path.getsize(path), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"bytes\"</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\">    loaded </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> joblib.load(path)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    same </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (loaded[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"model\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].predict_proba(churn[features]) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">==</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> champion.predict_proba(churn[features])).all()</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\">\"identical predictions after reload:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, same, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"| trained with scikit-learn\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, loaded[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"sklearn_version\"</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>لاحظ أن النموذج النهائي مدرَّب على العملاء الـ 398 ذوي الـ labels كلهم. فمهمة بيانات الاختبار كانت قياس عملية النمذجة؛ وبعد أن قستها، تعطي إعادة التدريب على كل صف ذي label النموذجَ المُطلَق شيئًا أكثر يتعلّم منه. سجّل أنك فعلت ذلك، واحتفظ بالمقياس المنشور من تقييم بيانات الاختبار.</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>ملفات <code dir=\"ltr\">joblib</code> هي ملفات pickle، وتحميل pickle قد ينفّذ كودًا اعتباطيًا. عامِل ملف النموذج كبرنامج: لا تحمّل إلا ما أنتجته بنفسك أو ما يأتي من مخزن موثوق ومحكوم الصلاحيات. وحمّله أيضًا بإصدار scikit-learn نفسه الذي درّبت به؛ فالإصدار المختلف قد يعطي التحذير <code dir=\"ltr\">InconsistentVersionWarning</code> أو يتصرّف بشكل مختلف. ثبّت الإصدار في ملف المتطلّبات.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">احرس المدخلات</h2>\n<p>سيقيّم النموذج الهراء بكل سرور. عميل لديه <code dir=\"ltr\">orders = 0</code>، أو <code dir=\"ltr\">mobile_share</code> يساوي 7، أو عمود مفقود، ينتج احتمالًا يبدو واثقًا كأي احتمال آخر. ضع طبقة رقيقة أمام <code dir=\"ltr\">predict_proba</code> تتحقق من المُدخل وتفشل بوضوح:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-index=\"1\"><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:#6E7781;--shiki-dark:#8B949E\"># assumes `import pandas as pd` and a loaded `bundle` as above</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">def</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> predict_churn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(record: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">dict</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, bundle: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">dict</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) -> </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">float</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    missing </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [f </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> f </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> bundle[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"features\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> f </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">not</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> record]</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> missing:</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">        raise</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ValueError</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\">\"missing fields: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">missing</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    if</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> not</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x3C;=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> record[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mobile_share\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;=</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:#CF222E;--shiki-dark:#FF7B72\">        raise</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ValueError</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mobile_share must be between 0 and 1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    row </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.DataFrame([record])[bundle[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"features\"</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:#0550AE;--shiki-dark:#79C0FF\"> float</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(bundle[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"model\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].predict_proba(row)[</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\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])</span></span></code></pre></div>\n<p>اختيار <code dir=\"ltr\">[bundle[&quot;features&quot;]]</code> يثبّت ترتيب الأعمدة، وهو ما يصرّ عليه scikit-learn كما بيّن القسم 1.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">مهمة دفعية أم API؟</h2>\n<p>هناك طريقتان شائعتان لتشغيل النموذج:</p>\n<ul>\n<li><strong>التقييم الدفعي (batch scoring)</strong>: مهمة مجدولة (ليلية أو أسبوعية) تحمّل النموذج، وتقيّم كل عميل نشط من مستودع البيانات، وتكتب الاحتمالات في جدول. وتقرأ أدوات التسويق الجدول. بسيطة، ورخيصة، وسهلة المراقبة، ومناسبة للانقطاع، حيث لا يكلّف القرار المتأخر يومًا شيئًا.</li>\n<li><strong>الـ API المباشر (online)</strong>: خدمة ويب تقيّم عميلًا واحدًا لكل طلب، في أجزاء من الثانية. وتلزم حين يحدث القرار أثناء جلسة المستخدم، مثل إظهار عرض استبقاء في صفحة الإلغاء.</li>\n</ul>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 250\" role=\"img\" aria-labelledby=\"l-ml-5-5-t1\"><defs><marker id=\"arrow-l-ml-5-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-5-5-t1\">ينتج pipeline التدريب ملف نموذج ذا إصدار. يغذّي ملف النموذج إما مهمة دفعية ليلية تكتب الدرجات في جدول لفريق التسويق، وإما API مباشرًا يجيب عن طلبات مفردة. وكلاهما يسجّل المدخلات والتنبؤات في المراقبة، التي تعود لتغذّي إعادة التدريب.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"95\" width=\"130\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"75\" y=\"120\" text-anchor=\"middle\">التدريب</text>\n  <text class=\"d-label-muted\" x=\"75\" y=\"140\" text-anchor=\"middle\">pipeline + CV</text>\n  <path class=\"d-arrow\" d=\"M140 123 L178 123\" marker-end=\"url(#arrow-l-ml-5-5-1)\"/>\n  <rect class=\"d-box-primary\" x=\"182\" y=\"95\" width=\"130\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"247\" y=\"120\" text-anchor=\"middle\">model.joblib</text>\n  <text class=\"d-label-muted\" x=\"247\" y=\"140\" text-anchor=\"middle\">ذو إصدار</text>\n  <path class=\"d-arrow\" d=\"M312 110 L360 62\" marker-end=\"url(#arrow-l-ml-5-5-1)\"/>\n  <path class=\"d-arrow\" d=\"M312 136 L360 184\" marker-end=\"url(#arrow-l-ml-5-5-1)\"/>\n  <rect class=\"d-box-accent\" x=\"364\" y=\"30\" width=\"160\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"444\" y=\"55\" text-anchor=\"middle\">مهمة دفعية ليلية</text>\n  <text class=\"d-label-muted\" x=\"444\" y=\"75\" text-anchor=\"middle\">الدرجات ← جدول</text>\n  <rect class=\"d-box-accent\" x=\"364\" y=\"160\" width=\"160\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"444\" y=\"185\" text-anchor=\"middle\">API مباشر</text>\n  <text class=\"d-label-muted\" x=\"444\" y=\"205\" text-anchor=\"middle\">طلب واحد، ms</text>\n  <path class=\"d-arrow\" d=\"M524 58 L568 110\" marker-end=\"url(#arrow-l-ml-5-5-1)\"/>\n  <path class=\"d-arrow\" d=\"M524 188 L568 136\" marker-end=\"url(#arrow-l-ml-5-5-1)\"/>\n  <rect class=\"d-box-warn\" x=\"572\" y=\"95\" width=\"118\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"631\" y=\"120\" text-anchor=\"middle\">المراقبة</text>\n  <text class=\"d-label-muted\" x=\"631\" y=\"140\" text-anchor=\"middle\">سجلّات، انجراف</text>\n  <path class=\"d-dashed d-line\" d=\"M631 151 C631 240 75 240 75 151\" fill=\"none\"/>\n  <text class=\"d-label-muted\" x=\"350\" y=\"244\" text-anchor=\"middle\">أعد التدريب حين تقول المراقبة ذلك</text>\n</svg><figcaption>من التدريب إلى القرارات</figcaption></figure>\n<p>وإذا احتجت فعلًا إلى API، فخدمة FastAPI صغيرة خيار شائع في Python. هذا المخطّط لا يعمل في المتصفّح، لكنه مكتمل بما يكفي لتشغيله محليًا بـ <code dir=\"ltr\">uvicorn app:app</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">app.py</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:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> joblib</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\"> fastapi </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> FastAPI</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pydantic </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> BaseModel, Field</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># joblib uses pickle: load only model files from your own trusted, access-controlled store</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">bundle </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> joblib.load(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churn_model.joblib\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">app </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> FastAPI()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">class</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Customer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">BaseModel</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">):</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    tenure_days: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">int</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Field(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ge</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\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    orders: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">int</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Field(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ge</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\">    total_spent: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">float</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Field(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ge</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\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    avg_order_value: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">float</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Field(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ge</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\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    days_since_last_order: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">int</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Field(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ge</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\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    used_coupon: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">int</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Field(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ge</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\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">le</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\">    support_tickets: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">int</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Field(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ge</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\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    mobile_share: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">float</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Field(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ge</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\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">le</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>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">@app.post</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"/predict\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">def</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> predict</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(customer: Customer):</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    row </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.DataFrame([customer.model_dump()])[bundle[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"features\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]]</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:#0550AE;--shiki-dark:#79C0FF\"> float</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(bundle[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"model\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].predict_proba(row)[</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\">1</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\"> {</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"churn_probability\"</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\">(prob, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"model_version\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: bundle[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"version\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]}</span></span></code></pre></div>\n<p>يتولّى نموذج Pydantic التحقق من المدخلات بشكل تصريحي: الطلب الذي فيه <code dir=\"ltr\">orders: 0</code> أو حقل مفقود يحصل على خطأ 422 قبل أن يراه النموذج أصلًا. وإعادة إصدار النموذج مع كل إجابة تتيح تتبّع أي تنبؤ إلى النموذج المحدّد الذي أنتجه. سجّل كل طلب واستجابة؛ فهذه السجلّات هي ما تعمل عليه فحوص الانجراف من الدرس السابق.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">هل نجح؟ أجرِ تجربة</h2>\n<p>الـ ROC AUC الجيد يعني أن النموذج يرتّب العملاء جيدًا. ولا يعني أن عرض الاسترجاع يغيّر رأي أحد. ولقياس ذلك، احجز شريحة عشوائية من العملاء المُعلَّمين (لنقل 20%) لا يتلقّون العرض. وبعد 90 يومًا، قارن الانقطاع بين المجموعة التي تواصلت معها والمجموعة المحجوزة. الفرق هو أثر العرض على العملاء الذين يختارهم النموذج، ومضروبًا في قيمة العميل يصبح الرقم الذي تهتم به الشركة فعلًا. ودون المجموعة المحجوزة، قد يكون أي انخفاض في الانقطاع بسبب الموسم، أو تعثّر منافس، أو الحظ.</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<h2 id=\"sec-5\" tabindex=\"-1\">أين أنت الآن</h2>\n<p>بدأت بسؤال وجدول، وصار لديك الآن إجراء يصمد: صُغ المسألة، وقسّم قبل أن تنظر، وتفوّق على نموذج وهمي للفئة الأكثر شيوعًا وعلى قاعدة، واختر المقاييس بحسب كلفة كل خطأ، وقارن النماذج بالتحقق المتقاطع داخل pipelines خالية من التسرّب، وتحقّق ممن يخدمهم النموذج بشكل أسوأ، وأطلق شيئًا تستطيع مراقبته واختباره. وتبيّن أن البطل هو أبسط نموذج جادّ، وأنت تعرف بالضبط لماذا. عادة السؤال “مقارنةً بماذا؟” هي أثمن ما كان لدى هذه الدورة لتعلّمه، وهي تنطبق دون تغيير على النموذج القادم الذي ستبنيه، أيًّا كان.</p>\n","toc":[{"id":"sec-1","text":"احفظ الـ pipeline، لا النموذج وحده","level":2},{"id":"sec-2","text":"احرس المدخلات","level":2},{"id":"sec-3","text":"مهمة دفعية أم API؟","level":2},{"id":"sec-4","text":"هل نجح؟ أجرِ تجربة","level":2},{"id":"sec-5","text":"أين أنت الآن","level":2}],"takeaways":["احفظ الـ pipeline المدرَّب كاملًا، لا النموذج وحده، كي تنتقل المعالجة المسبقة معه؛ وخزّن بجانبه قائمة الـ features ورقم إصدار وإصدار scikit-learn.","ملفات <code dir=\"ltr\">joblib</code> و<code dir=\"ltr\">pickle</code> قد تنفّذ كودًا عند تحميلها، فلا تحمّل إلا ملفات نماذج أنشأتها أنت أو تثق بها، وحمّلها بإصدار scikit-learn نفسه.","[object Object]","كثير من نماذج الأعمال، ومنها نموذج الانقطاع، يُطلق على أفضل وجه كمهمة دفعية مجدولة؛ أما الـ API المباشر فللقرارات التي يجب أن تحدث أثناء الطلب.","الاختبار الحقيقي للنموذج تجربة مضبوطة: قارن نتائج العملاء المُعلَّمين الذين تلقّوا العرض بمجموعة محجوزة عشوائيًا لم تتلقَّه."],"further":[{"title":"Model persistence","url":"https://scikit-learn.org/stable/model_persistence.html"},{"title":"pickle: security warning","url":"https://docs.python.org/3/library/pickle.html"}],"quiz":[{"q":"<p>لماذا نحفظ الـ pipeline المدرَّب بدل خطوة <code dir=\"ltr\">LogisticRegression</code> وحدها؟</p>","options":[{"text":"لأن ملف الـ pipeline أصغر.","why":"هو أكبر قليلًا إن كان ثمة فرق؛ والحجم ليس السبب."},{"text":"لأن المتوسطات والانحرافات المعيارية التي تعلّمها الـ scaler جزء من النموذج؛ ودونها ستُوحَّد مقاييس البيانات الجديدة بشكل مختلف.","why":"صحيح. يضمن الـ pipeline أن تطبّق بيئة الإنتاج المعالجة المسبقة نفسها تمامًا التي تدرّب عليها النموذج."},{"text":"لأن joblib لا يستطيع حفظ LogisticRegression وحده.","why":"يستطيع حفظ أي كائن قابل للـ pickle؛ والمسألة اتّساق، لا قدرة."},{"text":"لأن الـ pipelines تُحمَّل أسرع من الـ estimators المفردة.","why":"زمن التحميل لا يُذكر في الحالتين."}],"answer":1},{"q":"<p>أرسل لك زميل بالبريد الملف <code dir=\"ltr\">model.joblib</code> من مصدر مجهول وطلب منك تقييمه. ما الخطر؟</p>","options":[{"text":"تحميله قد ينفّذ كودًا اعتباطيًا على جهازك.","why":"صحيح. يستخدم joblib الـ pickle، الذي قد يشغّل كودًا أثناء التحميل. لا تحمّل الملفات إلا من مصادر تثق بها."},{"text":"قد يستهلك ذاكرة أكثر من اللازم عند فتحه.","why":"ممكن لكنه ثانوي؛ والخطر الجسيم هو تنفيذ الكود."},{"text":"سيعيد التدريب على بياناتك بصمت.","why":"التحميل لا يدرّب شيئًا. الخطر فيما يستطيع الملف تشغيله أثناء تحميله."}],"answer":0},{"q":"<p>يتواصل فريق التسويق مع كل عميل يُعلِّمه النموذج، فينخفض الانقطاع بينهم من 79% إلى 60%. هل يمكنك أن تنسب الفضل إلى النموذج والعرض؟</p>","options":[{"text":"نعم؛ انخفض الانقطاع بعد الإطلاق، فالنظام يعمل.","why":"ربما كان الانقطاع سينخفض على أي حال (الموسم، أو مشكلة لدى منافس). ودون مجموعة مقارنة لا يمكنك الجزم."},{"text":"نعم، ما دام الـ ROC AUC على الاختبار أعلى من 0.8.","why":"يُظهر الـ AUC أن النموذج يرتّب جيدًا؛ ولا يقول شيئًا عمّا إذا كان العرض يغيّر السلوك."},{"text":"لا، لأن الانقطاع يجب أن يُقاس على 30 يومًا لا 90.","why":"النافذة تعريف عملي؛ والمشكلة في غياب المقارنة."},{"text":"ليس بعد؛ تحتاج إلى مجموعة محجوزة عشوائيًا من العملاء المُعلَّمين لم تتلقَّ أي عرض لتقارن بها.","why":"صحيح. الفرق بين المجموعتين هو أثر العرض."}],"answer":3}],"exercise":{"kind":"python","files":["churn.csv"],"starter":"import io\n\nimport joblib\nimport pandas as pd\nimport sklearn\nfrom sklearn.linear_model import LogisticRegression\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\"]\nchampion = make_pipeline(StandardScaler(), LogisticRegression()).fit(churn[features], churn[\"churned\"])\n\ngood = {\"tenure_days\": 400, \"orders\": 2, \"total_spent\": 180.0, \"avg_order_value\": 90.0,\n        \"days_since_last_order\": 150, \"used_coupon\": 0, \"support_tickets\": 1, \"mobile_share\": 0.5}\n\n# 1. A bundle with the fitted pipeline, the feature list, a version string and sklearn's version\nbundle = {\"model\": champion}\n\n# 2. Save it with joblib to an in-memory buffer, then load it back into `reloaded`\nbuffer = io.BytesIO()\nreloaded = None\n\n# 3. Validate, then predict. Raise ValueError if any feature is missing,\n#    or if orders < 1, or if mobile_share is outside 0..1.\ndef predict_churn(record, bundle):\n    row = pd.DataFrame([record])\n    return bundle[\"model\"].predict_proba(row)[0, 1]\n","solution":"import io\n\nimport joblib\nimport pandas as pd\nimport sklearn\nfrom sklearn.linear_model import LogisticRegression\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\"]\nchampion = make_pipeline(StandardScaler(), LogisticRegression()).fit(churn[features], churn[\"churned\"])\n\ngood = {\"tenure_days\": 400, \"orders\": 2, \"total_spent\": 180.0, \"avg_order_value\": 90.0,\n        \"days_since_last_order\": 150, \"used_coupon\": 0, \"support_tickets\": 1, \"mobile_share\": 0.5}\n\nbundle = {\"model\": champion, \"features\": features,\n          \"version\": \"churn-2025-09-30.1\", \"sklearn_version\": sklearn.__version__}\n\nbuffer = io.BytesIO()\njoblib.dump(bundle, buffer)\nbuffer.seek(0)\nreloaded = joblib.load(buffer)\n\ndef predict_churn(record, bundle):\n    missing = [f for f in bundle[\"features\"] if f not in record]\n    if missing:\n        raise ValueError(f\"missing fields: {missing}\")\n    if record[\"orders\"] < 1:\n        raise ValueError(\"orders must be at least 1\")\n    if not 0 <= record[\"mobile_share\"] <= 1:\n        raise ValueError(\"mobile_share must be between 0 and 1\")\n    row = pd.DataFrame([record])[bundle[\"features\"]]\n    return float(bundle[\"model\"].predict_proba(row)[0, 1])\n\nprint(round(predict_churn(good, reloaded), 3))\n","checks":[{"id":"bundle","test":"set(bundle) >= {'model', 'features', 'version', 'sklearn_version'} and list(bundle['features']) == features and bundle['sklearn_version'] == sklearn.__version__"},{"id":"reloaded","test":"reloaded is not None and (reloaded['model'].predict_proba(churn[features]) == champion.predict_proba(churn[features])).all()"},{"id":"predicts","test":"isinstance(predict_churn(good, reloaded), float) and abs(predict_churn(good, reloaded) - float(champion.predict_proba(pd.DataFrame([good])[features])[0, 1])) < 1e-12 and abs(predict_churn(dict(reversed(list(good.items()))), reloaded) - predict_churn(good, reloaded)) < 1e-12"},{"id":"missing","test":"exec('try:\\n    predict_churn({k: v for k, v in good.items() if k != \"orders\"}, reloaded)\\n    _raised = False\\nexcept ValueError:\\n    _raised = True', globals()) or _raised"},{"id":"ranges","test":"exec('try:\\n    predict_churn({**good, \"mobile_share\": 7}, reloaded)\\n    _raised = False\\nexcept ValueError:\\n    _raised = True', globals()) or _raised and exec('try:\\n    predict_churn({**good, \"orders\": 0}, reloaded)\\n    _raised = False\\nexcept ValueError:\\n    _raised = True', globals()) or _raised"}],"title":"احزم البطل","prompt":"<p>جهّز البطل للإطلاق.</p>\n<ol>\n<li><code dir=\"ltr\">bundle</code>: قاموس فيه <code dir=\"ltr\">&quot;model&quot;</code> (الـ pipeline المدرَّب)، و<code dir=\"ltr\">&quot;features&quot;</code> (قائمة الـ features)، و<code dir=\"ltr\">&quot;version&quot;</code> (أي نص)، و<code dir=\"ltr\">&quot;sklearn_version&quot;</code> (<code dir=\"ltr\">sklearn.__version__</code>).</li>\n<li>احفظ <code dir=\"ltr\">bundle</code> بـ <code dir=\"ltr\">joblib.dump</code> في الـ <code dir=\"ltr\">buffer</code> الموجود في الذاكرة، وأرجعه إلى البداية، ثم حمّله من جديد في <code dir=\"ltr\">reloaded</code>.</li>\n<li>أكمل <code dir=\"ltr\">predict_churn(record, bundle)</code>: ارفع <code dir=\"ltr\">ValueError</code> إذا كان أي feature مفقودًا، أو كان <code dir=\"ltr\">orders</code> أقل من 1، أو كان <code dir=\"ltr\">mobile_share</code> خارج النطاق 0–1. وإلا فأعِد احتمال الانقطاع كـ <code dir=\"ltr\">float</code> في Python، مع ترتيب الأعمدة بحسب ترتيب الـ features في الحزمة (فمفاتيح السجلّ قد تأتي بأي ترتيب).</li>\n</ol>\n","hints":["<code dir=\"ltr\">joblib.dump(bundle, buffer)</code>، ثم <code dir=\"ltr\">buffer.seek(0)</code> قبل <code dir=\"ltr\">joblib.load(buffer)</code>: فالتحميل يقرأ من الموضع الحالي.","ابنِ <code dir=\"ltr\">missing = [f for f in bundle[&quot;features&quot;] if f not in record]</code> وارفع خطأً إذا لم تكن فارغة.","<code dir=\"ltr\">pd.DataFrame([record])[bundle[&quot;features&quot;]]</code> يثبّت ترتيب الأعمدة؛ وغلّف نتيجة <code dir=\"ltr\">predict_proba(...)[0, 1]</code> بـ <code dir=\"ltr\">float()</code>."],"explanation":"<p>تحمل الحزمة كل ما تحتاجه مهمة في بيئة الإنتاج: الـ pipeline مع الـ scaler المدرَّب، وترتيب الأعمدة، وإصدارًا يُسجَّل مع كل تنبؤ، وإصدار المكتبة لتثبيته. وإعادة التحميل تعطي تنبؤات متطابقة. ويحوّل الحارس المدخلات السيئة إلى خطأ صريح بدل احتمال يبدو معقولًا، واختيار <code dir=\"ltr\">bundle[&quot;features&quot;]</code> يجعل ترتيب الأعمدة مستقلًا عن طريقة بناء المستدعي للسجلّ. وفي خدمة حقيقية سيكون الـ buffer ملفًا في مخزن نماذج موثوق.</p>\n","checkLabels":{"bundle":"يحتوي bundle على model وfeatures وversion وsklearn_version","reloaded":"يتنبأ reloaded تمامًا كالأصل","predicts":"تُعيد predict_churn قيمة float الصحيحة، أيًّا كان ترتيب المفاتيح","missing":"الـ feature المفقود يرفع ValueError","ranges":"قيمة orders الأقل من 1 أو mobile_share خارج 0–1 ترفع ValueError"}},"words":673,"readMinutes":4}