{"id":"l-pda-5-4","courseId":"python-data-analysis","lang":"ar","kind":"wrapup","summary":"حوّل تحليلًا لمرة واحدة إلى تحليل قابل لإعادة الإنتاج باستخدام المُعامِلات والدوال والـ assertions والمخرجات المحفوظة و notebook نظيف يعمل من الأعلى إلى الأسفل، واعرف إلى أين تتجه بعد هذه الدورة.","html":"<p>إجابة الربع الرابع مكتوبة. بعد ثلاثة أشهر سيسأل رئيس النمو السؤال نفسه عن الربع الأول، وقد يكون من يجيب أنت، أو زميل، أو أنت وقد نسيت كل التفاصيل. التحليل الذي لا يعمل إلا في الـ notebook الذي وُلد فيه عمل لمرة واحدة. أما التحليل الذي يستطيع أيّ شخص إعادة تشغيله من الملفات الخام والحصول على الأرقام نفسها فهو أصل ذو قيمة. والفرق حفنة من العادات.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">شكل التحليل القابل لإعادة التشغيل</h2>\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>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># Parameters: everything that changes between runs lives here.</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">ORDERS_FILE</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"orders.csv\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">ITEMS_FILE</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"order_items.csv\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">QUARTER</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"2025Q4\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">EXCLUDE_STATUSES</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"cancelled\"</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\"> load_orders</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(orders_file, items_file):</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"\"\"One row per order with revenue (after discounts, no shipping).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    items </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(items_file)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    orders </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(orders_file, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">parse_dates</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_date\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    items[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"revenue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> items[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"quantity\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> items[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"unit_price\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> -</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> items[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"discount\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    revenue </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> items.groupby(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">as_index</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">False</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"revenue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].sum()</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders.merge(revenue, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">on</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">how</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"left\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">validate</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"one_to_one\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    assert</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].is_unique, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"duplicate orders\"</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    assert</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"revenue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].notna().all(), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders without items\"</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">def</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> quarter_summary</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(df, quarter, exclude):</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    kept </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">~</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"status\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].isin(exclude)]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    in_q </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> kept[kept[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_date\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].dt.to_period(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Q\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">==</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> quarter]</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    assert</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> len</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(in_q) </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:#CF222E;--shiki-dark:#FF7B72\">f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"no orders in </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">quarter</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">        \"quarter\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: quarter,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">        \"revenue\"</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\">(in_q[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"revenue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].sum(), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">        \"orders\"</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\">(in_q),</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">        \"customers\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: in_q[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"customer_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].nunique(),</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">        \"aov\"</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\">(in_q[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"revenue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].mean(), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">df </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> load_orders(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">ORDERS_FILE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">ITEMS_FILE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">summary </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.DataFrame([quarter_summary(df, q, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">EXCLUDE_STATUSES</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> q </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"2025Q3\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">QUARTER</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\">(summary)</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\">\"pandas </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">pd.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">__version__</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></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>أربع أفكار تحمل هذا الكود. <strong>المُعامِلات (parameters)</strong> في الأعلى تسمّي كل مُدخل وكل قرار، ومنها تعريف الإيراد، فيغيّر تشغيل الربع القادم سطرًا واحدًا. و<strong>الدوال</strong> تحمل الخطوات، وتأخذ مُدخلاتها كوسائط وتعيد النتائج، فلا تعتمد أبدًا على متغيّر متبقٍّ من مكان آخر. و<strong>الـ assertions</strong> تحدّد ما يجب أن يكون صحيحًا في البيانات قبل حساب أي شيء منها، كما فعلت في الدرس 3.5. و<strong>طباعة الإصدار</strong> تسجّل أيّ إصدار من pandas أنتج الأرقام، لأن القيم الافتراضية تتغيّر فعلًا بين الإصدارات الرئيسية.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">نظافة الـ notebook</h2>\n<p>الـ notebooks ممتازة للاستكشاف وخطيرة لإعداد التقارير، لأن الخلايا يمكن أن تُشغَّل بأي ترتيب، والمتغيّرات تبقى بعد زوال الخلايا التي صنعتها. قبل أن تشارك واحدًا:</p>\n<ul>\n<li><strong>شغّل Restart and Run All.</strong> إذا فشل أو تغيّرت الأرقام، فقد كان الـ notebook يعتمد على حالة مخفية.</li>\n<li><strong>اقرأ الملفات ولا تعدّلها أبدًا.</strong> احتفظ بالملفات المُصدَّرة الخام كما هي في مجلد <code dir=\"ltr\">data/raw</code>؛ التنظيف يحدث في الكود، فيمكن تكراره.</li>\n<li><strong>انقل الكود القابل لإعادة الاستخدام إلى الخارج.</strong> حين يحتاج اثنان من الـ notebooks إلى <code dir=\"ltr\">clean_orders</code>، فمكانها ملف <code dir=\"ltr\">.py</code> تستورده.</li>\n<li><strong>اكتب المخرجات في ملفات.</strong> تنتج <code dir=\"ltr\">summary.to_csv(&quot;q4_summary.csv&quot;, index=False)</code> الأرقام التي تستخدمها الشريحة؛ والنسخ من الشاشة يفتح الباب للأخطاء الكتابية.</li>\n<li><strong>اكتب التعريف بجانب الرقم.</strong> خلية Markdown قصيرة تقول ما الذي يشمله “الإيراد” توفّر عليك جدالًا لاحقًا.</li>\n</ul>\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>تشغّل <code dir=\"ltr\">df.pipe(clean).pipe(add_revenue).pipe(summarise)</code> دوالك بالترتيب، وكل دالة تستقبل نتيجة التي قبلها. تُقرأ مثل قائمة الخطوات التي ستشرحها لزميل، ويمكن اختبار كل خطوة وحدها.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">ما تستطيع فعله الآن</h2>\n<p>بدأت هذه الدورة وأنت لا تستطيع كتابة سطر Python واحد. أنت الآن تستطيع تحميل أي ملف CSV وفحصه، وتنظيف فوضى العالم الحقيقي (الخانات الفارغة، والأخطاء الكتابية، والمبالغ المكتوبة كنصوص، والتواريخ الملتبسة، والمكرّرات)، وربط الجداول وإعادة تشكيلها، وتلخيصها بحسب المجموعة وعبر الزمن، ووصف التوزيعات بصدق، وتحويل كل ذلك إلى إجابة مدعومة بالأدلة والتحفّظات. هذا هو العمل اليومي لمحلل البيانات.</p>\n<p>أين تتجه بعد ذلك يعتمد على الأسئلة التي تريد الإجابة عنها. تستخدم دورة <a href=\"/ar/courses/sql-for-analysts\">SQL للمحلّلين</a> بيانات Cartwheel نفسها داخل قاعدة بيانات، وهناك توجد معظم بيانات الشركات فعلًا. وتلتقط <a href=\"/ar/courses/ml-crash-course\">الدورة المكثّفة في تعلّم الآلة</a> الملف <code dir=\"ltr\">churn.csv</code> وتطرح السؤال التالي: أيّ العملاء على وشك الرحيل؟ أيًّا كان اختيارك، احتفظ بعادات هذه الدورة: افحص قبل أن تثق، واكتب التعريف، واجعل عملك قابلًا لإعادة التشغيل.</p>\n","toc":[{"id":"sec-1","text":"شكل التحليل القابل لإعادة التشغيل","level":2},{"id":"sec-2","text":"نظافة الـ notebook","level":2},{"id":"sec-3","text":"ما تستطيع فعله الآن","level":2}],"takeaways":["التحليل القابل لإعادة الإنتاج (reproducible) يعطي النتيجة نفسها حين يشغّله شخص آخر من الملفات الخام، من الأعلى إلى الأسفل، دون خطوات يدوية.","ضع المُدخلات والاختيارات في مُعامِلات مسمّاة في الأعلى (مسارات الملفات، والربع، وتعريف الإيراد)، كي تعني إعادة التشغيل لفترة جديدة تغيير سطر واحد.","الدوال تحمل الخطوات التي تكرّرها؛ والـ assertions تحدّد ما يجب أن تبدو عليه البيانات قبل الثقة بالنتائج.","استخدم Restart and Run All قبل مشاركة أي notebook، واحفظ النتائج بـ <code dir=\"ltr\">to_csv(..., index=False)</code> بدلًا من نسخ الأرقام بيدك."],"further":[{"title":"pandas.DataFrame.to_csv","url":"https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_csv.html"},{"title":"Python Tutorial - Modules","url":"https://docs.python.org/3/tutorial/modules.html"},{"title":"pandas.DataFrame.pipe","url":"https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pipe.html"}],"quiz":[{"q":"<p>يعطي notebook رقم الربع الرابع الصحيح على حاسوبك، لكن زميلك يحصل على رقم مختلف من الملف نفسه. ما السبب الأرجح؟</p>","options":[{"text":"تعطي pandas إجابات مختلفة قليلًا على الحواسيب المختلفة.","why":"pandas حتمية؛ الكود نفسه على البيانات نفسها يعطي النتيجة نفسها، باستثناء ضوضاء ضئيلة في الأعداد العشرية."},{"text":"يعتمد الـ notebook على خلايا شُغّلت بغير ترتيبها أو على متغيّر أُنشئ في خلية حُذفت لاحقًا.","why":"صحيح. الحالة المخفية في الـ notebook هي السبب الكلاسيكي. وتشغيل Restart and Run All قبل المشاركة يلتقطه."},{"text":"شاشة الزميل تقرّب الأرقام بشكل مختلف.","why":"تقريب العرض يغيّر شكل الرقم، لا الرقم الذي يُحسب."},{"text":"تتغيّر ملفات CSV حين تُنسخ.","why":"النسخ لا يغيّر محتوى الملف؛ النتيجة المختلفة تأتي من مسارات كود مختلفة أو ملفات مختلفة."}],"answer":1},{"q":"<p>في الربع القادم تحتاج التقرير نفسه للربع 2026Q1. في تحليل حسن البناء، ما الذي يجب أن تغيّره؟</p>","options":[{"text":"كل خلية تذكر 2025Q4","why":"البحث في الخلايا عن قيم مكتوبة يدويًا هو بالضبط ما وُجدت المُعامِلات لتجنّبه، وستفوتك واحدة."},{"text":"دوال التنظيف","why":"قواعد التنظيف لا تعتمد على الربع؛ وتغييرها لكل تقرير سيجعل النتائج غير قابلة للمقارنة."},{"text":"مُعامِل واحد في الأعلى، مثل <code dir=\"ltr\">QUARTER = &quot;2026Q1&quot;</code>، ومسار ملف المُدخلات إذا تغيّر","why":"صحيح. كل ما يليه يقرأ تلك الأسماء، فيتبعها التحليل كله."}],"answer":2},{"q":"<p>ماذا يضيف assertion مثل <code dir=\"ltr\">assert df[&quot;order_id&quot;].is_unique</code> إلى تحليل قابل لإعادة الإنتاج؟</p>","options":[{"text":"يجعل التحليل يتوقّف برسالة واضحة حين تكسر بيانات جديدة افتراضًا تعتمد عليه النتائج.","why":"صحيح. بدونه، سيضخّم ملف مُصدَّر مكرّر الإيراد بهدوء في تقرير الربع القادم."},{"text":"يحذف معرّفات الطلبات المكرّرة تلقائيًا.","why":"الـ assertions تفحص فقط؛ والتنظيف يقوم به كود مثل <code dir=\"ltr\">drop_duplicates()</code>."},{"text":"يسرّع الـ groupby التي تليه.","why":"تضيف الـ assertions قدرًا صغيرًا من العمل؛ قيمتها في الأمان لا في السرعة."}],"answer":0}],"exercise":{"kind":"python","files":["order_items.csv","orders.csv"],"starter":"import pandas as pd\n\nORDERS_FILE = \"orders.csv\"\nITEMS_FILE = \"order_items.csv\"\nQUARTERS = [\"2024Q4\", \"2025Q3\", \"2025Q4\"]\n\n\ndef load_orders(orders_file, items_file):\n    items = pd.read_csv(items_file)\n    orders = pd.read_csv(orders_file, parse_dates=[\"order_date\"])\n    items[\"revenue\"] = items[\"quantity\"] * items[\"unit_price\"] * (1 - items[\"discount\"])\n    revenue = items.groupby(\"order_id\", as_index=False)[\"revenue\"].sum()\n    return orders.merge(revenue, on=\"order_id\", how=\"left\", validate=\"one_to_one\")\n\n\ndef quarter_summary(df, quarter):\n    \"\"\"Return a dict with keys quarter, revenue, orders, aov for one quarter\n    (non-cancelled orders only), revenue and aov rounded to 2 decimals.\n    Assert that the quarter has at least one order.\"\"\"\n    return {}\n\n\ndf = load_orders(ORDERS_FILE, ITEMS_FILE)\n\n# summary: a DataFrame with one row per quarter in QUARTERS, built with quarter_summary\nsummary = None\n\n# Save summary to \"quarter_summary.csv\" without the index\n","solution":"import pandas as pd\n\nORDERS_FILE = \"orders.csv\"\nITEMS_FILE = \"order_items.csv\"\nQUARTERS = [\"2024Q4\", \"2025Q3\", \"2025Q4\"]\n\n\ndef load_orders(orders_file, items_file):\n    items = pd.read_csv(items_file)\n    orders = pd.read_csv(orders_file, parse_dates=[\"order_date\"])\n    items[\"revenue\"] = items[\"quantity\"] * items[\"unit_price\"] * (1 - items[\"discount\"])\n    revenue = items.groupby(\"order_id\", as_index=False)[\"revenue\"].sum()\n    return orders.merge(revenue, on=\"order_id\", how=\"left\", validate=\"one_to_one\")\n\n\ndef quarter_summary(df, quarter):\n    \"\"\"Return a dict with keys quarter, revenue, orders, aov for one quarter\n    (non-cancelled orders only), revenue and aov rounded to 2 decimals.\n    Assert that the quarter has at least one order.\"\"\"\n    kept = df[df[\"status\"] != \"cancelled\"]\n    in_q = kept[kept[\"order_date\"].dt.to_period(\"Q\") == quarter]\n    assert len(in_q) > 0, f\"no orders in {quarter}\"\n    return {\n        \"quarter\": quarter,\n        \"revenue\": round(in_q[\"revenue\"].sum(), 2),\n        \"orders\": len(in_q),\n        \"aov\": round(in_q[\"revenue\"].mean(), 2),\n    }\n\n\ndf = load_orders(ORDERS_FILE, ITEMS_FILE)\n\nsummary = pd.DataFrame([quarter_summary(df, q) for q in QUARTERS])\nsummary.to_csv(\"quarter_summary.csv\", index=False)\nprint(summary)\n","checks":[{"id":"q4","test":"(lambda r: r['quarter'] == '2025Q4' and abs(float(r['revenue']) - 146825.44) < 0.01 and int(r['orders']) == 642 and abs(float(r['aov']) - 228.70) < 0.01)(quarter_summary(df, '2025Q4'))"},{"id":"other-quarter","test":"(lambda r: int(r['orders']) == 228 and abs(float(r['revenue']) - 52537.10) < 0.01)(quarter_summary(df, '2024Q4'))"},{"id":"uses-argument","test":"int(quarter_summary(df[df['order_date'] < '2025-12-01'], '2025Q4')['orders']) == 457"},{"id":"summary","test":"isinstance(summary, pd.DataFrame) and list(summary['quarter']) == QUARTERS and {'revenue', 'orders', 'aov'} <= set(summary.columns)"},{"id":"saved","test":"__import__('os').path.exists('quarter_summary.csv') and len(pd.read_csv('quarter_summary.csv')) == 3 and 'Unnamed: 0' not in pd.read_csv('quarter_summary.csv').columns"}],"title":"اجعل التقرير الفصلي قابلًا لإعادة التشغيل","prompt":"<p>أكمل <code dir=\"ltr\">quarter_summary(df, quarter)</code> بحيث تعيد قاموسًا بالمفاتيح <code dir=\"ltr\">quarter</code> و<code dir=\"ltr\">revenue</code> و<code dir=\"ltr\">orders</code> و<code dir=\"ltr\">aov</code> للربع المعطى (نص مثل <code dir=\"ltr\">&quot;2025Q4&quot;</code>)، باستخدام الطلبات غير الملغاة فقط، مع تقريب <code dir=\"ltr\">revenue</code> و<code dir=\"ltr\">aov</code> إلى منزلتين عشريتين. يجب أن تستخدم الوسيط <code dir=\"ltr\">df</code>، لا متغيّرًا عامًا (global)، وأن تتأكّد بـ <code dir=\"ltr\">assert</code> من أن في الربع طلبًا واحدًا على الأقل. ثم ابنِ <code dir=\"ltr\">summary</code>، جدولًا (DataFrame) فيه صف لكل ربع في <code dir=\"ltr\">QUARTERS</code>، واحفظه في <code dir=\"ltr\">quarter_summary.csv</code> دون الفهرس.</p>\n","hints":["صفِّ مرتين داخل الدالة: احذف الطلبات الملغاة، ثم احتفظ بالصفوف التي يكون فيها <code dir=\"ltr\">df[&quot;order_date&quot;].dt.to_period(&quot;Q&quot;) == quarter</code>.","أعِد قاموسًا: <code dir=\"ltr\">{&quot;quarter&quot;: quarter, &quot;revenue&quot;: round(in_q[&quot;revenue&quot;].sum(), 2), &quot;orders&quot;: len(in_q), &quot;aov&quot;: round(in_q[&quot;revenue&quot;].mean(), 2)}</code>.","تحوّل <code dir=\"ltr\">pd.DataFrame([quarter_summary(df, q) for q in QUARTERS])</code> قائمة من القواميس إلى جدول (شكل قائمة القواميس من الدرس 1.3). واحفظ بـ <code dir=\"ltr\">summary.to_csv(&quot;quarter_summary.csv&quot;, index=False)</code>."],"explanation":"<p>تأخذ الدالة كل ما تحتاجه كوسائط، فيحصل الفحص الذي يمرّر لها جدولًا مُصفّى (طلبات ما قبل ديسمبر فقط) على إجابة مختلفة وصحيحة: 457 طلبًا لربع رابع جزئي. وهذا ما يجعل الدالة آمنة لإعادة الاستخدام. مقارنة عمود من نوع <code dir=\"ltr\">Period</code> بنص مثل <code dir=\"ltr\">&quot;2025Q4&quot;</code> تعمل لأن pandas تحوّل النص إلى فترة. وقائمة القواميس تصبح DataFrame مباشرة، صف لكل قاموس، وهذا يغلق الدائرة مع أول بنية بيانات قابلتها في هذه الدورة. وتُبقي <code dir=\"ltr\">index=False</code> الملف نظيفًا: بدونها يحصل ملف CSV على عمود إضافي بلا اسم فيه أرقام الصفوف.</p>\n","checkLabels":{"q4":"تعيد <code dir=\"ltr\">quarter_summary(df, &quot;2025Q4&quot;)</code> إيرادًا قدره 146,825.44 و642 طلبًا ومتوسط قيمة طلب 228.70","other-quarter":"تعمل مع أرباع أخرى أيضًا، مثل 2024Q4","uses-argument":"تستخدم <code dir=\"ltr\">df</code> المعطى لها، لا جدولًا عامًا","summary":"في <code dir=\"ltr\">summary</code> صف لكل ربع في <code dir=\"ltr\">QUARTERS</code>","saved":"حُفظ <code dir=\"ltr\">quarter_summary.csv</code> بثلاثة صفوف ودون عمود فهرس"}},"words":423,"readMinutes":3}