{"id":"l-pda-3-5","courseId":"python-data-analysis","lang":"ar","kind":"lesson","summary":"اعثر على الصفوف المكرّرة واحذفها بـ duplicated و drop_duplicates، ثم اجمع كل خطوات التنظيف في هذا القسم في دالة واحدة مع assertions تستطيع إعادة تشغيلها على ملف الشهر القادم.","html":"<p>تسعة صفوف في الملف الفوضوي تظهر مرتين. كل نسخة مكرّرة طلب حقيقي محسوب مرة أخرى، فكل مجموع حسبته من الملف المُصدَّر حتى الآن أعلى من حقيقته: 23,500.67 دولارًا بدلًا من 22,486.69، أي مبالغة بنسبة 4.5%. تأتي المكرّرات من ملفات أُعيد تصديرها، وصفوف نُسخت ولُصقت، وأنظمة تعيد محاولة رفع ملف بعد فشله. إنها آخر مشكلة في الملف، وإصلاحها يتيح لك تحويل هذا القسم كله إلى دالة واحدة.</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:#1F2328;--shiki-dark:#E6EDF3\">messy </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\">\"orders_messy.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">dupes </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> messy.duplicated()</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(dupes.sum(), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"duplicate rows\"</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\">both_copies </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> messy[messy.duplicated(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">keep</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\">)].sort_values(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_id\"</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\">(both_copies.head(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).to_string())</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>تعيد <code dir=\"ltr\">duplicated()</code> قناعًا قيمته <code dir=\"ltr\">True</code> لكل صف مطابق لصف سابق. لا توضع علامة على الظهور الأول (<code dir=\"ltr\">keep=&quot;first&quot;</code>)، وهذا ما تريده عند الحذف. أما للفحص، فتضع <code dir=\"ltr\">keep=False</code> علامة على كل النسخ، والترتيب بحسب المعرّف يضع كل زوج معًا. انظر إليها قبل حذف أي شيء: الصفان 22 و113 هما الطلب نفسه، والمنتج نفسه، والمجموع نفسه، ويفصل بينهما نحو مئة صف. هذا النمط، صفوف متطابقة متباعدة، نموذجي لملف مُصدَّر أُضيف إلى نفسه.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">حذفها</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"1\" aria-label=\"شغّل هذا المثال\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> 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:#1F2328;--shiki-dark:#E6EDF3\">messy </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\">\"orders_messy.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">deduped </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> messy.drop_duplicates()</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:#0550AE;--shiki-dark:#79C0FF\">len</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(messy), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"->\"</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\">(deduped))</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(deduped[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].is_unique)</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p>بعد حذف المكرّرات المطابقة تمامًا، يظهر كل <code dir=\"ltr\">order_id</code> مرة واحدة، وهذا يؤكّد أن المكرّرات كانت نسخًا كاملة لا نسختين مختلفتين من الطلب نفسه. لو كانت <code dir=\"ltr\">is_unique</code> تساوي <code dir=\"ltr\">False</code>، لكان لديك صفوف <strong>متعارضة</strong>: المعرّف نفسه مع مجموعين مختلفين مثلًا. وتلك تحتاج قرارًا، مثل الاحتفاظ بأحدث نسخة، وتطبّقه <code dir=\"ltr\">drop_duplicates(subset=[&quot;order_id&quot;], keep=&quot;last&quot;)</code> بعد أن تتّخذ ذلك القرار.</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\">order_items</code>، للطلب الواحد صف لكل منتج، فيتكرّر <code dir=\"ltr\">order_id</code> بشكل مشروع. ستحذف <code dir=\"ltr\">items.drop_duplicates(subset=[&quot;order_id&quot;])</code> 1,925 بندًا حقيقيًا دون أي تحذير. قبل اختيار subset، اسأل: ماذا يمثّل الصف الواحد؟ يجب أن يحدّد المفتاح ذلك بالضبط. وهنا هو <code dir=\"ltr\">order_item_id</code>.</p>\n</aside>\n<h3 id=\"sec-3\" tabindex=\"-1\">نظّف أولًا، ثم أزِل المكرّرات</h3>\n<p>اكتشاف المكرّرات يقارن القيم مقارنة دقيقة. <code dir=\"ltr\">&quot;Ali Evans&quot;</code> و<code dir=\"ltr\">&quot;Ali Evans &quot;</code> نصان مختلفان، و<code dir=\"ltr\">$22.00</code> و<code dir=\"ltr\">22.00</code> مجموعان مختلفان. في هذا الملف المُصدَّر تصادف أن النسخ متطابقة حرفًا بحرف، لكن بشكل عام تحذف المسافات، وتوحّد حالة الأحرف، وتحوّل الأرقام <strong>قبل</strong> البحث عن المكرّرات، وإلا نجت النسخ شبه المتطابقة.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">دالة واحدة للقسم كله</h2>\n<p>لديك الآن خمس خطوات تنظيف، مكتوبة عبر خمسة دروس. وحين تكون متفرّقة في notebook تكون هشّة: شغّل خلية مرتين أو تخطَّ واحدة، فتتغيّر النتيجة. ضعها في دالة واحدة، بالترتيب الصحيح، مع الفحوص في النهاية:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"2\" aria-label=\"شغّل هذا المثال\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pandas </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">COUNTRIES</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Egypt\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"United Arab Emirates\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Saudi Arabia\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Jordan\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">             \"United Kingdom\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Germany\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"United States\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Canada\"</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\"> parse_money</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(values):</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    cleaned </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (values.astype(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"string\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).str.strip()</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">               .str.replace(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"$\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">regex</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\">).str.replace(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\",\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">regex</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>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.to_numeric(cleaned, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">errors</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"coerce\"</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\"> parse_dates</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(raw):</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    iso </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.to_datetime(raw, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">format</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"%Y-%m-</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%d</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">errors</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"coerce\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    dmy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.to_datetime(raw, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">format</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%d</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/%m/%Y\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">errors</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"coerce\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    mdy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.to_datetime(raw, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">format</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"%m/</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%d</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/%Y\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">errors</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"coerce\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    ambiguous </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> dmy.notna() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> mdy.notna() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (dmy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> mdy)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> iso.fillna(dmy).fillna(mdy), ambiguous</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\"> clean_orders</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(path):</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"\"\"Load the messy export and return a clean, deduplicated DataFrame.\"\"\"</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\"> pd.read_csv(path)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"customer\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"customer\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].str.strip()</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"country\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"country\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].str.strip().str.title()</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"channel\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"channel\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].str.strip().str.replace(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\" \"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"_\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).fillna(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"unknown\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"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\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"quantity\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].astype(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Int64\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> parse_money(df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_date\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"date_is_ambiguous\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> parse_dates(df[</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\">    df </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df.drop_duplicates().reset_index(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">drop</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>\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 order ids remain\"</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_date\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].notna().all(), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"unparsed dates\"</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\">\"total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].notna().all() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">and</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </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\">).all(), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"bad totals\"</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    assert</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> set</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"country\"</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\"> COUNTRIES</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\">\"unknown countries: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">set</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'country'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNTRIES</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\"> df</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">clean </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> clean_orders(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders_messy.csv\"</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\">(clean.shape)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(clean.dtypes)</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:#0550AE;--shiki-dark:#79C0FF\">round</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(clean[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total\"</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 style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total sales,\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, clean[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"date_is_ambiguous\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].sum(), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"ambiguous dates\"</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>السطر <code dir=\"ltr\">df[&quot;order_date&quot;], df[&quot;date_is_ambiguous&quot;] = parse_dates(...)</code> يفكّ القيمتين اللتين تعيدهما الدالة المساعدة إلى عمودين دفعة واحدة. وتعيد <code dir=\"ltr\">reset_index(drop=True)</code> ترقيم الصفوف من 0 إلى 158 بعد الحذف، فتتّفق المواقع والتسميات من جديد. لاحظ الأنواع أيضًا: <code dir=\"ltr\">quantity</code> من النوع <code dir=\"ltr\">Int64</code> و<code dir=\"ltr\">total</code> من النوع <code dir=\"ltr\">Float64</code>، وكلاهما قابل للقيم المفقودة، لأن <code dir=\"ltr\">parse_money</code> تحوّل عبر النوع <code dir=\"ltr\">string</code> في pandas و<code dir=\"ltr\">to_numeric</code> تحافظ على العائلة القابلة للقيم المفقودة. وهما يتصرّفان كأرقام عادية في كل حساب ستجريه.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 200\" role=\"img\" aria-labelledby=\"l-pda-3-5-t1\"><defs><marker id=\"arrow-l-pda-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-pda-3-5-t1\">ملف مُصدَّر خام من 168 صفًا يمرّ عبر خمس خطوات: حذف المسافات وتوحيد النصوص، وتحويل نوع الكمية إلى Int64، وتحليل المبالغ، وتحليل التواريخ ووضع علامة على الملتبس، وحذف المكرّرات. ثم تفحص الـ assertions النتيجة، فيخرج 159 صفًا نظيفًا.</title>\n  <rect class=\"d-box-warn\" x=\"10\" y=\"60\" width=\"90\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"55\" y=\"86\" text-anchor=\"middle\">خام</text>\n  <text class=\"d-label\" x=\"55\" y=\"106\" text-anchor=\"middle\">168 صفًا</text>\n  <rect class=\"d-box\" x=\"120\" y=\"60\" width=\"80\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"160\" y=\"95\" text-anchor=\"middle\">النصوص</text>\n  <rect class=\"d-box\" x=\"215\" y=\"60\" width=\"80\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"255\" y=\"95\" text-anchor=\"middle\">Int64</text>\n  <rect class=\"d-box\" x=\"310\" y=\"60\" width=\"80\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"350\" y=\"95\" text-anchor=\"middle\">المبالغ</text>\n  <rect class=\"d-box\" x=\"405\" y=\"60\" width=\"80\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"445\" y=\"95\" text-anchor=\"middle\">التواريخ</text>\n  <rect class=\"d-box\" x=\"500\" y=\"60\" width=\"80\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"540\" y=\"95\" text-anchor=\"middle\">المكرّرات</text>\n  <rect class=\"d-box-success\" x=\"600\" y=\"60\" width=\"90\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"645\" y=\"86\" text-anchor=\"middle\">نظيف</text>\n  <text class=\"d-label\" x=\"645\" y=\"106\" text-anchor=\"middle\">159 صفًا</text>\n  <path class=\"d-arrow\" d=\"M100 90 L118 90\" marker-end=\"url(#arrow-l-pda-3-5-1)\"/>\n  <path class=\"d-arrow\" d=\"M200 90 L213 90\" marker-end=\"url(#arrow-l-pda-3-5-1)\"/>\n  <path class=\"d-arrow\" d=\"M295 90 L308 90\" marker-end=\"url(#arrow-l-pda-3-5-1)\"/>\n  <path class=\"d-arrow\" d=\"M390 90 L403 90\" marker-end=\"url(#arrow-l-pda-3-5-1)\"/>\n  <path class=\"d-arrow\" d=\"M485 90 L498 90\" marker-end=\"url(#arrow-l-pda-3-5-1)\"/>\n  <path class=\"d-arrow\" d=\"M580 90 L598 90\" marker-end=\"url(#arrow-l-pda-3-5-1)\"/>\n  <rect class=\"d-box-primary\" x=\"500\" y=\"140\" width=\"190\" height=\"40\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"595\" y=\"165\" text-anchor=\"middle\">الـ assertions تحرس المخرج</text>\n  <text class=\"d-label-muted\" x=\"350\" y=\"40\" text-anchor=\"middle\">clean_orders(path)</text>\n</svg><figcaption>خط التنظيف: 168 صفًا فوضويًا تدخل، و159 صفًا نظيفًا تخرج</figcaption></figure>\n<p>تقرأ الدالة ملفها بنفسها، فلا تعتمد أبدًا على ما تصادف أنك شغّلته سابقًا في notebook. المُدخل نفسه، المُخرج نفسه، في كل مرة. وحين يصل ملف الشهر القادم، تشغّل <code dir=\"ltr\">clean_orders(&quot;orders_messy_2026_01.csv&quot;)</code> القسم كله في سطر واحد، وإذا احتوى الملف الجديد على كتابة تاسعة لدولة أو مجموع باليورو، يوقفه assertion برسالة تسمّي المشكلة.</p>\n<p>أين تضع الدالة؟ أثناء الاستكشاف، في أول خلية من الـ notebook. وحين يحتاجها notebook ثانٍ أو زميل، انقلها إلى ملفها الخاص، مثل <code dir=\"ltr\">cleaning.py</code> بجانب ملفات الـ notebook، واكتب <code dir=\"ltr\">from cleaning import clean_orders</code> حيثما احتجتها. تعريف واحد مستورَد في كل مكان يعني أن الإصلاح الذي تجريه مرة يصل إلى كل تحليل يعتمد عليه.</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>الـ assertions توثيق يعمل</span></p>\n<p>كل سطر <code dir=\"ltr\">assert</code> يقول ما معنى “نظيف” لهذه البيانات، بصيغة يستطيع زميل قراءتها ويستطيع الحاسوب فحصها. حين تكتشف نوعًا جديدًا من المشكلات، أضِف إصلاحًا وassertion معًا. يعود الدرس 5.4 إلى هذا حين تجعل تحليلًا كاملًا قابلًا لإعادة الإنتاج.</p>\n</aside>\n<p>أصبح الملف المُصدَّر نظيفًا: 159 طلبًا، و22,486.69 دولارًا من المبيعات، و25 تاريخًا عليها علامة الالتباس (كانت 30 قبل حذف المكرّرات). ينتقل القسم 4 من التنظيف إلى الإجابة، بدءًا بأكثر أدوات pandas استخدامًا: التجميع (<code dir=\"ltr\">groupby</code>).</p>\n","toc":[{"id":"sec-1","text":"العثور على المكرّرات","level":2},{"id":"sec-2","text":"حذفها","level":2},{"id":"sec-3","text":"نظّف أولًا، ثم أزِل المكرّرات","level":3},{"id":"sec-4","text":"دالة واحدة للقسم كله","level":2}],"takeaways":["تضع <code dir=\"ltr\">df.duplicated()</code> علامة على التكرارات المطابقة تمامًا بعد ظهورها الأول؛ و<code dir=\"ltr\">keep=False</code> تضع علامة على كل النسخ لتفحصها جنبًا إلى جنب.","تزيل <code dir=\"ltr\">drop_duplicates(subset=[...])</code> المكرّرات بحسب أعمدة مفتاحية، لكن استخدم فقط مفتاحًا فريدًا فعلًا لكل صف، ولا تستخدم أبدًا <code dir=\"ltr\">order_id</code> في جدول بنود الطلبات.","نظّف النصوص قبل البحث عن المكرّرات، لأن <code dir=\"ltr\">&quot;Ali Evans&quot;</code> و<code dir=\"ltr\">&quot;Ali Evans &quot;</code> نصان مختلفان.","دالة التنظيف تحوّل جلسة notebook لمرة واحدة إلى خطوة قابلة للتكرار - المُدخل نفسه، المُخرج نفسه، كل شهر.","اختم الدالة بجمل <code dir=\"ltr\">assert</code> تعبّر عن معنى “نظيف”، كي توقف البيانات السيئة خط المعالجة بدلًا من أن تصل إلى تقرير."],"further":[{"title":"pandas.DataFrame.drop_duplicates","url":"https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.drop_duplicates.html"},{"title":"pandas.DataFrame.duplicated","url":"https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.duplicated.html"},{"title":"The assert statement","url":"https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement"}],"quiz":[{"q":"<p>في جدول صفان متطابقان للطلب 1026. ماذا تعدّ <code dir=\"ltr\">df.duplicated().sum()</code> لهما؟</p>","options":[{"text":"2، واحد لكل نسخة","why":"مع القيمة الافتراضية <code dir=\"ltr\">keep=&quot;first&quot;</code> لا توضع علامة على النسخة الأولى؛ بل على التكرارات فقط."},{"text":"0، لأن للصفين تسميتي فهرس مختلفتين","why":"تقارن <code dir=\"ltr\">duplicated()</code> قيم الأعمدة، لا الفهرس."},{"text":"1، النسخة الثانية فقط","why":"صحيح. يُحتفظ بالظهور الأول على أنه الأصل، وتوضع علامة على كل صف مطابق يأتي بعده. استخدم <code dir=\"ltr\">keep=False</code> لوضع علامة على الاثنين."}],"answer":2},{"q":"<p>لماذا تكون <code dir=\"ltr\">items.drop_duplicates(subset=[&quot;order_id&quot;])</code> خطيرة على <code dir=\"ltr\">order_items</code>؟</p>","options":[{"text":"الطلب الذي فيه عدة منتجات له عدة بنود مشروعة، وستُحذف كلها ما عدا الأول.","why":"صحيح. على بيانات Cartwheel يحذف هذا 1,925 بندًا حقيقيًا. أزِل المكرّرات بحسب مفتاح يحدّد صفًا واحدًا، مثل <code dir=\"ltr\">order_item_id</code>."},{"text":"تُطلق خطأً لأن <code dir=\"ltr\">order_id</code> رقمي.","why":"يمكن استخدام أيّ نوع عمود كمفتاح في subset؛ الخطر هو فقدان بيانات صامت، لا خطأ."},{"text":"تغيّر الجدول الأصلي في مكانه.","why":"تعيد <code dir=\"ltr\">drop_duplicates</code> جدولًا (DataFrame) جديدًا؛ والخطر فيما ينقص ذلك الجدول الجديد."}],"answer":0},{"q":"<p>ما الفائدة الأساسية من ختم دالة التنظيف بـ <code dir=\"ltr\">assert df[&quot;order_id&quot;].is_unique</code>؟</p>","options":[{"text":"تحذف أيّ معرّفات مكرّرة متبقية.","why":"الـ assertion لا يغيّر البيانات أبدًا؛ إنه يفحص شرطًا فقط."},{"text":"تجعل الدالة تعمل أسرع.","why":"يضيف الفحص قدرًا ضئيلًا من العمل. قيمته في الأمان لا في السرعة."},{"text":"تخزّن النتيجة كي يستخدمها الدرس التالي.","why":"الـ assertions لا تحفظ شيئًا؛ و<code dir=\"ltr\">return</code> في الدالة هي التي تعيد البيانات."},{"text":"إذا كسر ملف مُصدَّر في المستقبل القاعدة، يتوقّف الـ script بخطأ بدلًا من إنتاج تقرير خاطئ.","why":"صحيح. يحوّل الـ assertion الافتراض إلى حقيقة مُختبَرة في كل مرة تعمل فيها الدالة."}],"answer":3}],"exercise":{"kind":"python","files":["orders_messy.csv"],"starter":"import pandas as pd\n\ndef parse_money(values):\n    cleaned = (values.astype(\"string\").str.strip()\n               .str.replace(\"$\", \"\", regex=False).str.replace(\",\", \"\", regex=False))\n    return pd.to_numeric(cleaned, errors=\"coerce\")\n\n\ndef clean_orders(path):\n    \"\"\"Return the messy export cleaned and deduplicated.\"\"\"\n    df = pd.read_csv(path)\n    # strip customer; strip + title-case country\n    # channel: \"mobile app\" -> \"mobile_app\", missing -> \"unknown\"\n    # total: parse_money\n    # order_date: pd.to_datetime(..., format=\"mixed\", dayfirst=True)\n    # drop exact duplicates and reset the index\n    # assert that order_id is unique\n    return df\n\n\nclean = clean_orders(\"orders_messy.csv\")\n\n# Sales per cleaned country, as a dict {country: total rounded to 2 decimals}\nsales_by_country = None\n","solution":"import pandas as pd\n\ndef parse_money(values):\n    cleaned = (values.astype(\"string\").str.strip()\n               .str.replace(\"$\", \"\", regex=False).str.replace(\",\", \"\", regex=False))\n    return pd.to_numeric(cleaned, errors=\"coerce\")\n\n\ndef clean_orders(path):\n    \"\"\"Return the messy export cleaned and deduplicated.\"\"\"\n    df = pd.read_csv(path)\n    df[\"customer\"] = df[\"customer\"].str.strip()\n    df[\"country\"] = df[\"country\"].str.strip().str.title()\n    df[\"channel\"] = df[\"channel\"].str.replace(\" \", \"_\").fillna(\"unknown\")\n    df[\"total\"] = parse_money(df[\"total\"])\n    df[\"order_date\"] = pd.to_datetime(df[\"order_date\"], format=\"mixed\", dayfirst=True)\n    df = df.drop_duplicates().reset_index(drop=True)\n    assert df[\"order_id\"].is_unique, \"duplicate order ids remain\"\n    return df\n\n\nclean = clean_orders(\"orders_messy.csv\")\n\nsales_by_country = {}\nfor country in clean[\"country\"].unique():\n    sales_by_country[country] = round(float(clean.loc[clean[\"country\"] == country, \"total\"].sum()), 2)\nprint(sales_by_country)\n","checks":[{"id":"rows","test":"isinstance(clean, pd.DataFrame) and len(clean) == 159 and clean['order_id'].is_unique"},{"id":"index","test":"list(clean.index) == list(range(159))"},{"id":"text","test":"clean['country'].nunique() == 8 and set(clean['channel']) == {'web', 'mobile_app', 'marketplace', 'unknown'} and (clean['customer'] == clean['customer'].str.strip()).all()"},{"id":"types","test":"pd.api.types.is_numeric_dtype(clean['total']) and pd.api.types.is_datetime64_any_dtype(clean['order_date'])"},{"id":"total","test":"round(float(clean['total'].sum()), 2) == 22486.69"},{"id":"by-country","test":"isinstance(sales_by_country, dict) and len(sales_by_country) == 8 and round(sum(sales_by_country.values()), 2) == 22486.69"},{"id":"reusable","test":"len(clean_orders('orders_messy.csv')) == 159"}],"title":"اكتب clean_orders()","prompt":"<p>أكمل <code dir=\"ltr\">clean_orders(path)</code> بحيث تحمّل الملف المُصدَّر وتعيده منظّفًا: احذف المسافات من <code dir=\"ltr\">customer</code>؛ واحذف المسافات من <code dir=\"ltr\">country</code> وحوّله إلى حالة العنوان؛ وحوّل <code dir=\"ltr\">mobile app</code> إلى <code dir=\"ltr\">mobile_app</code> والقنوات المفقودة إلى <code dir=\"ltr\">&quot;unknown&quot;</code>؛ وحوّل <code dir=\"ltr\">total</code> بالدالة المعطاة <code dir=\"ltr\">parse_money</code>؛ وحلّل <code dir=\"ltr\">order_date</code> بـ <code dir=\"ltr\">pd.to_datetime(..., format=&quot;mixed&quot;, dayfirst=True)</code>؛ واحذف المكرّرات المطابقة تمامًا وأعِد ترقيم الفهرس من 0؛ واستخدم <code dir=\"ltr\">assert</code> للتأكّد من أن <code dir=\"ltr\">order_id</code> فريد قبل الإعادة. ثم اجعل <code dir=\"ltr\">sales_by_country</code> قاموسًا يربط كل دولة منظّفة بإجمالي مبيعاتها، مقرّبًا إلى منزلتين عشريتين.</p>\n","hints":["كل خطوة سطر واحد داخل الدالة، يُسند النتيجة إلى <code dir=\"ltr\">df[...]</code>. نفّذ تنظيف النصوص والمبالغ قبل <code dir=\"ltr\">drop_duplicates()</code>.","<code dir=\"ltr\">df = df.drop_duplicates().reset_index(drop=True)</code> تحذف النسخ وتعيد ترقيم الصفوف من 0 إلى 158. ثم <code dir=\"ltr\">assert df[&quot;order_id&quot;].is_unique, &quot;duplicate order ids remain&quot;</code>.","للقاموس، مرّ على <code dir=\"ltr\">clean[&quot;country&quot;].unique()</code> واجمع <code dir=\"ltr\">clean.loc[clean[&quot;country&quot;] == country, &quot;total&quot;]</code>، كما في الدرس 1.4. (يعرض الدرس 4.1 طريقة <code dir=\"ltr\">groupby</code> من سطر واحد لهذا.)"],"explanation":"<p>تقرأ الدالة ملفها بنفسها وتطبّق كل إصلاح من هذا القسم بترتيب ثابت، فتشغيلها مرتين، أو على ملف الشهر القادم، يعطي النوع نفسه من النتائج. يحدث التنظيف قبل إزالة المكرّرات كي تتطابق النسخ شبه المتطابقة أيضًا؛ وهنا كانت النسخ التسع مطابقة تمامًا. وهناك اختصار يجب أن تكون صريحًا بشأنه: ما زال <code dir=\"ltr\">format=&quot;mixed&quot;, dayfirst=True</code> يخطئ في قراءة التواريخ الخمسة المكتوبة بالشهر أولًا التي اكتشفتها في الدرس 3.4، لذا تحتفظ النسخة المعتمدة في العمل الحقيقي بعلامة الالتباس أو تصلح تلك الصفوف من نظام الطلبات. والـ assertion هو عقد الدالة: إذا احتوى ملف مُصدَّر مستقبلي على صفين مختلفين لطلب واحد، فإنها تتوقّف بدلًا من إعادة بيانات سيئة. ومجاميع الدول تعود فتساوي 22,486.69، رقم المبيعات بعد إزالة المكرّرات، وهذا فحص “الأجزاء تساوي الكل” من الدرس 1.4 مرة أخرى. في الدرس التالي، تحلّ <code dir=\"ltr\">clean.groupby(&quot;country&quot;)[&quot;total&quot;].sum()</code> محل الحلقة.</p>\n","checkLabels":{"rows":"في <code dir=\"ltr\">clean</code> 159 صفًا، صف لكل طلب","index":"أُعيد ترقيم الفهرس من 0 إلى 158","text":"الدول والقنوات وأسماء العملاء منظّفة","types":"<code dir=\"ltr\">total</code> رقمي و<code dir=\"ltr\">order_date</code> من نوع datetime","total":"إجمالي المبيعات 22,486.69","by-country":"في <code dir=\"ltr\">sales_by_country</code> 8 دول مجموعها 22,486.69","reusable":"استدعاء <code dir=\"ltr\">clean_orders</code> مرة أخرى يعطي الصفوف الـ 159 نفسها"}},"words":600,"readMinutes":4}