{"id":"l-pda-3-5","courseId":"python-data-analysis","lang":"en","kind":"lesson","summary":"Find and remove duplicate rows with duplicated and drop_duplicates, then combine every cleaning step from this section into one function with assertions that you can rerun on next month’s export.","html":"<p>Nine rows in the messy export appear twice. Each duplicate is a real order counted again, so every total you have calculated from the export so far is too high: 23,500.67 dollars instead of 22,486.69, an overstatement of 4.5%. Duplicates come from re-run exports, copy-pasted rows and systems that retry a failed upload. They are the last problem in the file, and fixing them lets you turn this whole section into a single function.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Finding duplicates</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=\"Run this example\"><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>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><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>Copy</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> returns a mask that is <code dir=\"ltr\">True</code> for every row identical to an earlier one. The first appearance is not flagged (<code dir=\"ltr\">keep=&quot;first&quot;</code>), which is what you want for removal. For inspection, <code dir=\"ltr\">keep=False</code> flags every copy, and sorting by the id puts each pair together. Look at them before deleting anything: rows 22 and 113 are the same order, same product, same total, almost a hundred rows apart. That pattern, identical rows far apart, is typical of an export that was appended to itself.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Removing them</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=\"Run this example\"><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>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><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>Copy</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>After dropping exact duplicates, every <code dir=\"ltr\">order_id</code> appears once, which confirms that the duplicates were full copies and not two different versions of the same order. If <code dir=\"ltr\">is_unique</code> had been <code dir=\"ltr\">False</code>, you would have <strong>conflicting</strong> rows: the same id with, say, two different totals. Those need a decision, such as keeping the latest version, and <code dir=\"ltr\">drop_duplicates(subset=[&quot;order_id&quot;], keep=&quot;last&quot;)</code> implements it once you have made that decision.</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\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Deduplicating on the wrong key</span></p>\n<p>In <code dir=\"ltr\">order_items</code>, one order has one row per product, so <code dir=\"ltr\">order_id</code> repeats legitimately. <code dir=\"ltr\">items.drop_duplicates(subset=[&quot;order_id&quot;])</code> would delete 1,925 real lines without any warning. Before choosing a subset, ask what one row represents; the key must identify exactly that. Here it is <code dir=\"ltr\">order_item_id</code>.</p>\n</aside>\n<h3 id=\"sec-3\" tabindex=\"-1\">Clean first, deduplicate second</h3>\n<p>Duplicate detection compares values exactly. <code dir=\"ltr\">&quot;Ali Evans&quot;</code> and <code dir=\"ltr\">&quot;Ali Evans &quot;</code> are different strings, and <code dir=\"ltr\">$22.00</code> and <code dir=\"ltr\">22.00</code> are different totals. In this export the copies happen to be byte-for-byte identical, but in general you strip text, standardise case and convert numbers <strong>before</strong> looking for duplicates, or near-identical copies survive.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">One function for the whole section</h2>\n<p>You now have five cleaning steps, written across five lessons. Scattered across a notebook, they are fragile: run a cell twice or skip one, and the result changes. Put them in one function, in the right order, with the checks at the end:</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=\"Run this example\"><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>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><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>Copy</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> unpacks the two values the helper returns into two columns at once. <code dir=\"ltr\">reset_index(drop=True)</code> renumbers the rows 0 to 158 after the drop, so positions and labels agree again. Notice the dtypes too: <code dir=\"ltr\">quantity</code> is <code dir=\"ltr\">Int64</code> and <code dir=\"ltr\">total</code> is <code dir=\"ltr\">Float64</code>, both nullable, because <code dir=\"ltr\">parse_money</code> converts through pandas’ <code dir=\"ltr\">string</code> type and <code dir=\"ltr\">to_numeric</code> keeps the nullable family. They behave like ordinary numbers in every calculation you will do.</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\">Raw export of 168 rows flows through five steps: strip and standardise text, retype quantity as Int64, parse money, parse dates and flag ambiguity, drop duplicates. Then assertions check the result, and 159 clean rows come out.</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\">raw</text>\n  <text class=\"d-label\" x=\"55\" y=\"106\" text-anchor=\"middle\">168 rows</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</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\">money</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\">dates</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\">dedupe</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\">clean</text>\n  <text class=\"d-label\" x=\"645\" y=\"106\" text-anchor=\"middle\">159 rows</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 guard the exit</text>\n  <text class=\"d-label-muted\" x=\"350\" y=\"40\" text-anchor=\"middle\">clean_orders(path)</text>\n</svg><figcaption>The cleaning pipeline: 168 messy rows in, 159 clean rows out</figcaption></figure>\n<p>The function reads its own file, so it never depends on what you happened to run earlier in a notebook. Same input, same output, every time. When next month’s export arrives, <code dir=\"ltr\">clean_orders(&quot;orders_messy_2026_01.csv&quot;)</code> runs the whole section in one line, and if the new file contains a ninth country spelling or a total in euros, an assertion stops it with a message naming the problem.</p>\n<p>Where should the function live? While you are exploring, in the first cell of your notebook. Once a second notebook or a colleague needs it, move it to its own file, say <code dir=\"ltr\">cleaning.py</code> next to your notebooks, and write <code dir=\"ltr\">from cleaning import clean_orders</code> wherever you need it. One definition, imported everywhere, means a fix made once reaches every analysis that depends on it.</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\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Assertions are documentation that runs</span></p>\n<p>Each <code dir=\"ltr\">assert</code> line says what “clean” means for this dataset, in a form a colleague can read and the computer can check. When you discover a new kind of problem, add a fix and an assertion together. Lesson 5.4 returns to this when you make a whole analysis reproducible.</p>\n</aside>\n<p>The export is clean: 159 orders, 22,486.69 dollars of sales, and 25 dates flagged as ambiguous (30 before the duplicates went). Section 4 moves from cleaning to answering, starting with the most used tool in pandas: <code dir=\"ltr\">groupby</code>.</p>\n","toc":[{"id":"sec-1","text":"Finding duplicates","level":2},{"id":"sec-2","text":"Removing them","level":2},{"id":"sec-3","text":"Clean first, deduplicate second","level":3},{"id":"sec-4","text":"One function for the whole section","level":2}],"takeaways":["<code dir=\"ltr\">df.duplicated()</code> flags exact repeats after their first appearance; <code dir=\"ltr\">keep=False</code> flags every copy so you can inspect them side by side.","<code dir=\"ltr\">drop_duplicates(subset=[...])</code> deduplicates on key columns, but only use a key that is truly unique per row, never <code dir=\"ltr\">order_id</code> in a table of order lines.","Clean text before looking for duplicates, because <code dir=\"ltr\">&quot;Ali Evans&quot;</code> and <code dir=\"ltr\">&quot;Ali Evans &quot;</code> are different strings.","A cleaning function turns a one-off notebook session into a repeatable step - same input, same output, every month.","End the function with <code dir=\"ltr\">assert</code> statements that encode what clean means, so bad data stops the pipeline instead of reaching a report."],"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>A table has two identical rows for order 1026. What does <code dir=\"ltr\">df.duplicated().sum()</code> count for them?</p>","options":[{"text":"2, one for each copy","why":"With the default <code dir=\"ltr\">keep=&quot;first&quot;</code>, the first copy is not flagged; only repeats are."},{"text":"0, because the rows have different index labels","why":"<code dir=\"ltr\">duplicated()</code> compares the column values, not the index."},{"text":"1, the second copy only","why":"Correct. The first appearance is kept as the original, and each later identical row is flagged. Use <code dir=\"ltr\">keep=False</code> to flag both."}],"answer":2},{"q":"<p>Why is <code dir=\"ltr\">items.drop_duplicates(subset=[&quot;order_id&quot;])</code> dangerous on <code dir=\"ltr\">order_items</code>?</p>","options":[{"text":"An order with several products has several legitimate lines, and all but the first would be deleted.","why":"Correct. On Cartwheel’s data that removes 1,925 real lines. Deduplicate on a key that identifies one row, such as <code dir=\"ltr\">order_item_id</code>."},{"text":"It raises an error because <code dir=\"ltr\">order_id</code> is numeric.","why":"Any column type can be used as a subset key; the risk is silent data loss, not an error."},{"text":"It changes the original table in place.","why":"<code dir=\"ltr\">drop_duplicates</code> returns a new DataFrame; the danger is in what that new table is missing."}],"answer":0},{"q":"<p>What is the main benefit of ending a cleaning function with <code dir=\"ltr\">assert df[&quot;order_id&quot;].is_unique</code>?</p>","options":[{"text":"It removes any remaining duplicate ids.","why":"An assertion never changes data; it only checks a condition."},{"text":"It makes the function run faster.","why":"The check adds a tiny amount of work. Its value is safety, not speed."},{"text":"It stores the result so the next lesson can use it.","why":"Assertions do not save anything; the function’s <code dir=\"ltr\">return</code> hands back the data."},{"text":"If a future export breaks the rule, the script stops with an error instead of producing a wrong report.","why":"Correct. The assertion turns an assumption into a tested fact every time the function runs."}],"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":"Write clean_orders()","prompt":"<p>Complete <code dir=\"ltr\">clean_orders(path)</code> so it loads the export and returns it cleaned: strip <code dir=\"ltr\">customer</code>; strip and title-case <code dir=\"ltr\">country</code>; turn <code dir=\"ltr\">mobile app</code> into <code dir=\"ltr\">mobile_app</code> and missing channels into <code dir=\"ltr\">&quot;unknown&quot;</code>; convert <code dir=\"ltr\">total</code> with the provided <code dir=\"ltr\">parse_money</code>; parse <code dir=\"ltr\">order_date</code> with <code dir=\"ltr\">pd.to_datetime(..., format=&quot;mixed&quot;, dayfirst=True)</code>; drop exact duplicates and renumber the index from 0; and <code dir=\"ltr\">assert</code> that <code dir=\"ltr\">order_id</code> is unique before returning. Then set <code dir=\"ltr\">sales_by_country</code> to a dictionary mapping each cleaned country to its total sales, rounded to 2 decimals.</p>\n","hints":["Each step is one line inside the function, assigning back to <code dir=\"ltr\">df[...]</code>. Do the text and money cleaning before <code dir=\"ltr\">drop_duplicates()</code>.","<code dir=\"ltr\">df = df.drop_duplicates().reset_index(drop=True)</code> removes the copies and renumbers rows 0 to 158. Then <code dir=\"ltr\">assert df[&quot;order_id&quot;].is_unique, &quot;duplicate order ids remain&quot;</code>.","For the dictionary, loop over <code dir=\"ltr\">clean[&quot;country&quot;].unique()</code> and sum <code dir=\"ltr\">clean.loc[clean[&quot;country&quot;] == country, &quot;total&quot;]</code>, as in Lesson 1.4. (Lesson 4.1 shows a one-line <code dir=\"ltr\">groupby</code> for this.)"],"explanation":"<p>The function reads its own file and applies every fix from this section in a fixed order, so running it twice, or on next month’s file, gives the same kind of result. Cleaning happens before deduplication so near-identical copies would also match; here the nine copies were exact. One shortcut to be honest about: <code dir=\"ltr\">format=&quot;mixed&quot;, dayfirst=True</code> still misreads the five month-first dates you caught in Lesson 3.4, so a production version would keep the ambiguity flag or repair those rows from the order system. The assertion is the function’s contract: if a future export contains two different rows for one order, it stops instead of returning bad data. The country totals add back up to 22,486.69, the deduplicated sales figure, which is the parts-equal-the-whole check from Lesson 1.4 again. In the next lesson, <code dir=\"ltr\">clean.groupby(&quot;country&quot;)[&quot;total&quot;].sum()</code> replaces the loop.</p>\n","checkLabels":{"rows":"<code dir=\"ltr\">clean</code> has 159 rows, one per order","index":"The index is renumbered 0 to 158","text":"Countries, channels and customer names are cleaned","types":"<code dir=\"ltr\">total</code> is numeric and <code dir=\"ltr\">order_date</code> is a datetime","total":"Total sales are 22,486.69","by-country":"<code dir=\"ltr\">sales_by_country</code> has 8 countries adding up to 22,486.69","reusable":"Calling <code dir=\"ltr\">clean_orders</code> again gives the same 159 rows"}},"words":689,"readMinutes":3}