{"id":"l-pda-4-2","courseId":"python-data-analysis","lang":"en","kind":"lesson","summary":"Combine tables with merge, choose the right join type, protect yourself from duplicated rows with validate= and row counts, and use indicator=True to find records with no match.","html":"<p>The head of growth’s question needs columns from four tables. Revenue lives in <code dir=\"ltr\">order_items</code>. Whether an order was cancelled, and when it was placed, lives in <code dir=\"ltr\">orders</code>. The product’s category lives in <code dir=\"ltr\">products</code>, and the customer’s country in <code dir=\"ltr\">customers</code>. In a spreadsheet you would write lookup formulas; in pandas you <strong>merge</strong>. Merges are where most silent errors in analysis happen, so this lesson spends as much time on checking a merge as on writing one.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">A first merge</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\">items </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\">\"order_items.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">orders </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </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>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">lines </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> items.merge(orders, </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\">\"many_to_one\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">len</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(items), </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\">(lines))</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(lines[[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_item_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_id\"</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\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_date\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"status\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]].head(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</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>For each row of the left table, pandas looks up the row of <code dir=\"ltr\">orders</code> with the same <code dir=\"ltr\">order_id</code> and copies its columns across. Many item lines point to one order, so the relationship is <strong>many to one</strong>, and <code dir=\"ltr\">validate=&quot;many_to_one&quot;</code> tells pandas to check that: if <code dir=\"ltr\">orders</code> had two rows for one id, the merge would raise <code dir=\"ltr\">MergeError</code> instead of quietly doubling those items. The row count before and after is the second check. A left merge onto a unique key never changes it.</p>\n<p>Now revenue can finally respect the business rule you postponed in Lesson 2.5: cancelled orders are not revenue.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"1\" aria-label=\"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\">items </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\">\"order_items.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">orders </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </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\">lines </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> items.merge(orders, </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\">\"many_to_one\"</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\">kept </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> lines[lines[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"status\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"cancelled\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">round</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(kept[</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 style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"revenue excluding cancelled orders\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p>446,666.32 dollars. This is the revenue definition for the rest of the course: line revenue after discounts, excluding shipping fees and cancelled orders. Returned orders stay in, because their refunds are tracked separately in <code dir=\"ltr\">returns</code>. Write definitions like this down where readers will see them; a number without its definition invites an argument.</p>\n<p>When the key has a different name on each side, name both: <code dir=\"ltr\">left_on=&quot;customer_id&quot;, right_on=&quot;referred_by&quot;</code> in a merge of <code dir=\"ltr\">customers</code> with itself matches each customer to the people they referred. And pick the key that identifies what you are matching. Returns belong to order <strong>lines</strong>, so <code dir=\"ltr\">returns</code> joins <code dir=\"ltr\">order_items</code> on <code dir=\"ltr\">order_item_id</code>; joining it on <code dir=\"ltr\">order_id</code> instead would attach each refund to every line of the order.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Join types</h2>\n<p><code dir=\"ltr\">how</code> decides what happens to keys that exist on only one side:</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 220\" role=\"img\" aria-labelledby=\"l-pda-4-2-t1\"><defs><marker id=\"arrow-l-pda-4-2-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-4-2-t1\">Left table keys A, B, C and right table keys B, C, D. Inner keeps B and C. Left keeps A, B, C with A's right columns empty. Right keeps B, C, D. Outer keeps A, B, C, D.</title>\n  <text class=\"d-label-strong\" x=\"60\" y=\"28\" text-anchor=\"middle\">left</text>\n  <rect class=\"d-box-primary\" x=\"30\" y=\"40\" width=\"60\" height=\"30\" rx=\"5\"/><text class=\"d-code\" x=\"60\" y=\"60\" text-anchor=\"middle\">A</text>\n  <rect class=\"d-box-primary\" x=\"30\" y=\"76\" width=\"60\" height=\"30\" rx=\"5\"/><text class=\"d-code\" x=\"60\" y=\"96\" text-anchor=\"middle\">B</text>\n  <rect class=\"d-box-primary\" x=\"30\" y=\"112\" width=\"60\" height=\"30\" rx=\"5\"/><text class=\"d-code\" x=\"60\" y=\"132\" text-anchor=\"middle\">C</text>\n  <text class=\"d-label-strong\" x=\"150\" y=\"28\" text-anchor=\"middle\">right</text>\n  <rect class=\"d-box-accent\" x=\"120\" y=\"76\" width=\"60\" height=\"30\" rx=\"5\"/><text class=\"d-code\" x=\"150\" y=\"96\" text-anchor=\"middle\">B</text>\n  <rect class=\"d-box-accent\" x=\"120\" y=\"112\" width=\"60\" height=\"30\" rx=\"5\"/><text class=\"d-code\" x=\"150\" y=\"132\" text-anchor=\"middle\">C</text>\n  <rect class=\"d-box-accent\" x=\"120\" y=\"148\" width=\"60\" height=\"30\" rx=\"5\"/><text class=\"d-code\" x=\"150\" y=\"168\" text-anchor=\"middle\">D</text>\n  <path class=\"d-line\" d=\"M210 30 L210 200\"/>\n  <text class=\"d-label-strong\" x=\"270\" y=\"28\" text-anchor=\"middle\">inner</text>\n  <text class=\"d-code\" x=\"270\" y=\"96\" text-anchor=\"middle\">B</text><text class=\"d-code\" x=\"270\" y=\"132\" text-anchor=\"middle\">C</text>\n  <text class=\"d-label-strong\" x=\"380\" y=\"28\" text-anchor=\"middle\">left</text>\n  <text class=\"d-code\" x=\"380\" y=\"60\" text-anchor=\"middle\">A + NaN</text><text class=\"d-code\" x=\"380\" y=\"96\" text-anchor=\"middle\">B</text><text class=\"d-code\" x=\"380\" y=\"132\" text-anchor=\"middle\">C</text>\n  <text class=\"d-label-strong\" x=\"490\" y=\"28\" text-anchor=\"middle\">right</text>\n  <text class=\"d-code\" x=\"490\" y=\"96\" text-anchor=\"middle\">B</text><text class=\"d-code\" x=\"490\" y=\"132\" text-anchor=\"middle\">C</text><text class=\"d-code\" x=\"490\" y=\"168\" text-anchor=\"middle\">NaN + D</text>\n  <text class=\"d-label-strong\" x=\"610\" y=\"28\" text-anchor=\"middle\">outer</text>\n  <text class=\"d-code\" x=\"610\" y=\"60\" text-anchor=\"middle\">A + NaN</text><text class=\"d-code\" x=\"610\" y=\"96\" text-anchor=\"middle\">B</text><text class=\"d-code\" x=\"610\" y=\"132\" text-anchor=\"middle\">C</text><text class=\"d-code\" x=\"610\" y=\"168\" text-anchor=\"middle\">NaN + D</text>\n  <text class=\"d-label-muted\" x=\"440\" y=\"208\" text-anchor=\"middle\">missing partners are filled with NaN</text>\n</svg><figcaption>Four join types, by which keys survive</figcaption></figure>\n<p><code dir=\"ltr\">inner</code> (the default) keeps only keys found on both sides. <code dir=\"ltr\">left</code> keeps every left row, filling the right-hand columns with <code dir=\"ltr\">NaN</code> where there is no partner. <code dir=\"ltr\">right</code> is the mirror image, and <code dir=\"ltr\">outer</code> keeps everything. For adding information to a table of facts, such as orders, items or sessions, use <code dir=\"ltr\">how=&quot;left&quot;</code> with the fact table on the left: you never lose a fact because a lookup table is incomplete, and the <code dir=\"ltr\">NaN</code>s show you exactly where it is.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Finding what did not match</h2>\n<p><code dir=\"ltr\">indicator=True</code> adds a <code dir=\"ltr\">_merge</code> column recording where each row came from:</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:#1F2328;--shiki-dark:#E6EDF3\">customers </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\">\"customers.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">orders </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">buyers </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders[[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"customer_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]].drop_duplicates()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">check </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> customers.merge(buyers, </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\">\"customer_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\">indicator</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">True</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(check[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"_merge\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].value_counts())</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">never_ordered </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> check[check[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"_merge\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">==</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"left_only\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">len</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(never_ordered), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"customers never ordered\"</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>85 customers signed up and never bought anything, the same number you found with <code dir=\"ltr\">nunique</code> in Lesson 2.2, now as a list of names you could hand to the marketing team. <code dir=\"ltr\">indicator=True</code> is also the fastest audit of any merge: <code dir=\"ltr\">right_only</code> rows in a left join are impossible, and a surprising number of <code dir=\"ltr\">left_only</code> rows means keys that should match do not, often because of types (<code dir=\"ltr\">int</code> on one side, text on the other) or untrimmed spaces.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Same column name on both sides</h2>\n<p><code dir=\"ltr\">order_items</code> and <code dir=\"ltr\">products</code> both have a <code dir=\"ltr\">unit_price</code> column: the price charged and the list price. pandas adds suffixes to tell them apart, and you can choose them:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"3\"><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=\"3\" 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\">items </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\">\"order_items.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">orders </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </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\">products </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\">\"products.csv\"</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\">lines </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    items.merge(orders[[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_id\"</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 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\">validate</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"many_to_one\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    .merge(products[[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"product_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"name\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"unit_price\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]], </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\">\"product_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">           suffixes</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"_charged\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"_list\"</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\">\"many_to_one\"</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 style=\"color:#1F2328;--shiki-dark:#E6EDF3\">lines[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"price_ratio\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (lines[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"unit_price_charged\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> lines[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"unit_price_list\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]).round(</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:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(lines.groupby(lines[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_date\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">>=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"2025-03-01\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"price_ratio\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].agg([</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"min\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"max\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]))</span></span></code></pre><div class=\"run-output\" data-run-output=\"3\" hidden></div></div>\n<p>Before March 2025, every line was charged exactly the list price; from 1 March, every line was charged 5% more. The merge has just confirmed the price rise from the data alone, and it is one of the suspects for the Q4 revenue jump.</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>Joining on a key that repeats on both sides</span></p>\n<p><code dir=\"ltr\">orders.merge(tickets, on=&quot;customer_id&quot;)</code> returns 6,624 rows from 2,066 orders and 873 tickets: every order of a customer is paired with every ticket of that customer. Sum revenue on that and you count orders several times over. <code dir=\"ltr\">validate=&quot;many_to_one&quot;</code> turns this into an immediate <code dir=\"ltr\">MergeError</code>. Write it on every merge where you expect the right side to be unique; it costs nothing and catches the most expensive mistake in this lesson.</p>\n</aside>\n<p>In the exercise you build the full line-level table that the rest of the course analyses. Next you reshape tables between wide and long formats with <code dir=\"ltr\">pivot_table</code> and <code dir=\"ltr\">melt</code>.</p>\n","toc":[{"id":"sec-1","text":"A first merge","level":2},{"id":"sec-2","text":"Join types","level":2},{"id":"sec-3","text":"Finding what did not match","level":2},{"id":"sec-4","text":"Same column name on both sides","level":2}],"takeaways":["<code dir=\"ltr\">left.merge(right, on=&quot;key&quot;, how=...)</code> combines tables by matching key values; <code dir=\"ltr\">how</code> decides what happens to rows without a match.","<code dir=\"ltr\">how=&quot;left&quot;</code> keeps every row of the left table and is the safe default for adding columns to a fact table.","Pass <code dir=\"ltr\">validate=&quot;many_to_one&quot;</code> (or <code dir=\"ltr\">&quot;one_to_one&quot;</code>) so pandas raises <code dir=\"ltr\">MergeError</code> if the right-hand keys are not unique, instead of silently multiplying rows.","Compare <code dir=\"ltr\">len()</code> before and after every merge; a left merge onto a unique key never changes the row count.","<code dir=\"ltr\">indicator=True</code> adds a <code dir=\"ltr\">_merge</code> column (<code dir=\"ltr\">both</code>, <code dir=\"ltr\">left_only</code>, <code dir=\"ltr\">right_only</code>) that shows which rows found a partner."],"further":[{"title":"Merge, join, concatenate and compare","url":"https://pandas.pydata.org/docs/user_guide/merging.html"},{"title":"pandas.DataFrame.merge","url":"https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.merge.html"}],"quiz":[{"q":"<p>You left-merge <code dir=\"ltr\">order_items</code> (3,991 rows) with <code dir=\"ltr\">orders</code> on <code dir=\"ltr\">order_id</code>. How many rows should the result have?</p>","options":[{"text":"2,066, one per order","why":"A merge does not aggregate; every item row stays a row. Grouping is what reduces rows."},{"text":"More than 3,991, because orders have several items","why":"Each item matches exactly one order, so nothing is multiplied. Duplication happens when the right-hand key repeats."},{"text":"Fewer than 3,991, because cancelled orders are dropped","why":"Merges match keys; they do not filter by status. You filter afterwards."},{"text":"Exactly 3,991, because each item has one matching order","why":"Correct. Many items to one order, with <code dir=\"ltr\">how=&quot;left&quot;</code>, keeps the left table’s row count. <code dir=\"ltr\">validate=&quot;many_to_one&quot;</code> guarantees it."}],"answer":3},{"q":"<p>Which merge finds the customers who never placed an order?</p>","options":[{"text":"<code dir=\"ltr\">customers.merge(orders, on=&quot;customer_id&quot;, how=&quot;inner&quot;)</code>","why":"An inner join keeps only customers with at least one order, which is the opposite of what you want."},{"text":"<code dir=\"ltr\">customers.merge(orders, on=&quot;customer_id&quot;, how=&quot;left&quot;, indicator=True)</code>, then keep <code dir=\"ltr\">_merge == &quot;left_only&quot;</code>","why":"Correct. A left join keeps every customer, and <code dir=\"ltr\">left_only</code> marks those with no matching order. That finds 85 customers."},{"text":"<code dir=\"ltr\">orders.merge(customers, on=&quot;customer_id&quot;, how=&quot;left&quot;)</code>","why":"With orders on the left, every row is an order, so customers without orders never appear."}],"answer":1},{"q":"<p><code dir=\"ltr\">orders.merge(tickets, on=&quot;customer_id&quot;)</code> returns 6,624 rows from 2,066 orders and 873 tickets. What happened?</p>","options":[{"text":"<code dir=\"ltr\">customer_id</code> repeats on both sides, so every order of a customer was paired with every ticket of that customer.","why":"Correct. This is a many-to-many join. <code dir=\"ltr\">validate=&quot;many_to_one&quot;</code> would have raised an error; join tickets to orders on <code dir=\"ltr\">order_id</code> instead."},{"text":"pandas added blank rows for tickets without orders.","why":"The default <code dir=\"ltr\">how=&quot;inner&quot;</code> adds no unmatched rows at all."},{"text":"The merge appended the two tables one below the other.","why":"That is <code dir=\"ltr\">pd.concat</code>. A merge matches rows side by side."}],"answer":0}],"exercise":{"kind":"python","files":["order_items.csv","orders.csv","products.csv","customers.csv"],"starter":"import pandas as pd\n\nitems = pd.read_csv(\"order_items.csv\")\norders = pd.read_csv(\"orders.csv\", parse_dates=[\"order_date\"])\nproducts = pd.read_csv(\"products.csv\")\ncustomers = pd.read_csv(\"customers.csv\")\nitems[\"revenue\"] = items[\"quantity\"] * items[\"unit_price\"] * (1 - items[\"discount\"])\n\n# 1. sales: every item line with its order's columns, the product name (as \"product\"),\n#    and the customer's country. Use left merges with validate=\"many_to_one\".\n#    Then drop the lines of cancelled orders.\nsales = None\n\n# 2. Revenue by country, a Series sorted from highest to lowest\nrevenue_by_country = None\n","solution":"import pandas as pd\n\nitems = pd.read_csv(\"order_items.csv\")\norders = pd.read_csv(\"orders.csv\", parse_dates=[\"order_date\"])\nproducts = pd.read_csv(\"products.csv\")\ncustomers = pd.read_csv(\"customers.csv\")\nitems[\"revenue\"] = items[\"quantity\"] * items[\"unit_price\"] * (1 - items[\"discount\"])\n\nsales = (\n    items.merge(orders, on=\"order_id\", how=\"left\", validate=\"many_to_one\")\n    .merge(products[[\"product_id\", \"name\"]].rename(columns={\"name\": \"product\"}),\n           on=\"product_id\", how=\"left\", validate=\"many_to_one\")\n    .merge(customers[[\"customer_id\", \"country\"]], on=\"customer_id\", how=\"left\", validate=\"many_to_one\")\n)\nprint(len(sales))\nsales = sales[sales[\"status\"] != \"cancelled\"]\n\nrevenue_by_country = sales.groupby(\"country\")[\"revenue\"].sum().sort_values(ascending=False)\nprint(revenue_by_country.round(2))\n","checks":[{"id":"columns","test":"isinstance(sales, pd.DataFrame) and {'revenue', 'order_date', 'status', 'product', 'country'} <= set(sales.columns)"},{"id":"no-cancelled","test":"(sales['status'] != 'cancelled').all()"},{"id":"row-count","test":"len(sales) == int((items['order_id'].map(orders.set_index('order_id')['status']) != 'cancelled').sum())"},{"id":"revenue-total","test":"round(float(sales['revenue'].sum()), 2) == 446666.32"},{"id":"by-country","test":"isinstance(revenue_by_country, pd.Series) and revenue_by_country.index[0] == 'United Arab Emirates' and abs(float(revenue_by_country.iloc[0]) - 74531.8) < 0.01 and revenue_by_country.is_monotonic_decreasing"}],"title":"Build the sales table","prompt":"<p>Build <code dir=\"ltr\">sales</code>: every row of <code dir=\"ltr\">items</code>, plus its order’s columns from <code dir=\"ltr\">orders</code>, the product’s <code dir=\"ltr\">name</code> renamed to <code dir=\"ltr\">product</code>, and the customer’s <code dir=\"ltr\">country</code>. Use left merges with <code dir=\"ltr\">validate=&quot;many_to_one&quot;</code>, then remove the lines of cancelled orders. Then set <code dir=\"ltr\">revenue_by_country</code> to total revenue per country as a Series, sorted from highest to lowest.</p>\n","hints":["Chain merges: <code dir=\"ltr\">items.merge(orders, on=&quot;order_id&quot;, how=&quot;left&quot;, validate=&quot;many_to_one&quot;)</code>, then products on <code dir=\"ltr\">product_id</code>, then customers on <code dir=\"ltr\">customer_id</code>.","Select only the columns you need from the lookup tables, e.g. <code dir=\"ltr\">products[[&quot;product_id&quot;, &quot;name&quot;]].rename(columns={&quot;name&quot;: &quot;product&quot;})</code>, so you don’t import clashing names like <code dir=\"ltr\">unit_price</code>.","Filter with <code dir=\"ltr\">sales[sales[&quot;status&quot;] != &quot;cancelled&quot;]</code>, then <code dir=\"ltr\">groupby(&quot;country&quot;)[&quot;revenue&quot;].sum().sort_values(ascending=False)</code>."],"explanation":"<p>Each merge adds columns without changing the number of rows, because every item has exactly one order, one product and one customer, which <code dir=\"ltr\">validate=&quot;many_to_one&quot;</code> guarantees. Selecting only the lookup columns you need keeps the table tidy and avoids <code dir=\"ltr\">_x</code>/<code dir=\"ltr\">_y</code> suffixes. After removing cancelled orders the revenue is 446,666.32 dollars, the course’s revenue definition. The UAE leads with 74,531.80 dollars, just ahead of the UK, even though the US has more customers: a first sign that revenue per customer differs by country.</p>\n","checkLabels":{"columns":"<code dir=\"ltr\">sales</code> has revenue, order, product and country columns","no-cancelled":"No cancelled orders remain","row-count":"Every non-cancelled item line is present exactly once","revenue-total":"Total revenue is 446,666.32","by-country":"<code dir=\"ltr\">revenue_by_country</code> is sorted, with the UAE first at 74,531.80"}},"words":693,"readMinutes":3}