{"id":"l-pda-4-3","courseId":"python-data-analysis","lang":"en","kind":"lesson","summary":"Reshape data between long and wide formats - pivot_table for cross-tab summaries, crosstab for counts and rates, and melt to turn spreadsheet-style wide tables back into analysable long ones.","html":"<p>A manager does not want 3,991 rows. They want a grid: channels down the side, quarters across the top, revenue in the cells. The finance team sends targets the same way, one column per quarter. Your analysis, meanwhile, works best on long tables with one row per observation, because <code dir=\"ltr\">groupby</code>, <code dir=\"ltr\">merge</code> and filtering all expect that shape. Reshaping moves data between the two forms, and it is the last core skill before you start answering questions in earnest.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 220\" role=\"img\" aria-labelledby=\"l-pda-4-3-t1\"><defs><marker id=\"arrow-l-pda-4-3-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-3-t1\">On the left, a long table with columns quarter, channel and revenue has six rows. An arrow labelled pivot_table points to a wide table with quarters as rows and channels web and app as columns. A return arrow labelled melt points back.</title>\n  <text class=\"d-label-strong\" x=\"130\" y=\"24\" text-anchor=\"middle\">long</text>\n  <rect class=\"d-box-primary\" x=\"30\" y=\"34\" width=\"200\" height=\"26\" rx=\"5\"/><text class=\"d-code\" x=\"130\" y=\"52\" text-anchor=\"middle\">quarter channel revenue</text>\n  <rect class=\"d-box\" x=\"30\" y=\"64\" width=\"200\" height=\"24\" rx=\"5\"/><text class=\"d-code\" x=\"130\" y=\"81\" text-anchor=\"middle\">Q1  web   27,868</text>\n  <rect class=\"d-box\" x=\"30\" y=\"92\" width=\"200\" height=\"24\" rx=\"5\"/><text class=\"d-code\" x=\"130\" y=\"109\" text-anchor=\"middle\">Q1  app   16,814</text>\n  <rect class=\"d-box\" x=\"30\" y=\"120\" width=\"200\" height=\"24\" rx=\"5\"/><text class=\"d-code\" x=\"130\" y=\"137\" text-anchor=\"middle\">Q2  web   30,110</text>\n  <rect class=\"d-box\" x=\"30\" y=\"148\" width=\"200\" height=\"24\" rx=\"5\"/><text class=\"d-code\" x=\"130\" y=\"165\" text-anchor=\"middle\">Q2  app   15,840</text>\n  <text class=\"d-label-muted\" x=\"130\" y=\"196\" text-anchor=\"middle\">one row per observation</text>\n  <path class=\"d-arrow\" d=\"M250 80 L440 80\" marker-end=\"url(#arrow-l-pda-4-3-1)\"/>\n  <text class=\"d-code\" x=\"345\" y=\"70\" text-anchor=\"middle\">pivot_table</text>\n  <path class=\"d-arrow\" d=\"M440 140 L250 140\" marker-end=\"url(#arrow-l-pda-4-3-1)\"/>\n  <text class=\"d-code\" x=\"345\" y=\"162\" text-anchor=\"middle\">melt</text>\n  <text class=\"d-label-strong\" x=\"560\" y=\"24\" text-anchor=\"middle\">wide</text>\n  <rect class=\"d-box-primary\" x=\"460\" y=\"34\" width=\"200\" height=\"26\" rx=\"5\"/><text class=\"d-code\" x=\"560\" y=\"52\" text-anchor=\"middle\">quarter  web    app</text>\n  <rect class=\"d-box\" x=\"460\" y=\"64\" width=\"200\" height=\"24\" rx=\"5\"/><text class=\"d-code\" x=\"560\" y=\"81\" text-anchor=\"middle\">Q1   27,868 16,814</text>\n  <rect class=\"d-box\" x=\"460\" y=\"92\" width=\"200\" height=\"24\" rx=\"5\"/><text class=\"d-code\" x=\"560\" y=\"109\" text-anchor=\"middle\">Q2   30,110 15,840</text>\n  <text class=\"d-label-muted\" x=\"560\" y=\"196\" text-anchor=\"middle\">one column per channel</text>\n</svg><figcaption>Long and wide hold the same data in different shapes</figcaption></figure>\n<h2 id=\"sec-1\" tabindex=\"-1\">pivot_table: summarise and reshape at once</h2>\n<p>Start from the sales table you built in Lesson 4.2 (here rebuilt in a few lines) and ask for revenue by channel and year:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"0\" aria-label=\"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\">sales </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:#1F2328;--shiki-dark:#E6EDF3\">sales </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sales[sales[</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:#1F2328;--shiki-dark:#E6EDF3\">sales </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sales.assign(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">year</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">sales[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_date\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].dt.year, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">quarter</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">sales[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_date\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].dt.quarter)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">table </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sales.pivot_table(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">index</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"channel\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">columns</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"year\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">values</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"revenue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">                          aggfunc</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"sum\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">margins</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 style=\"color:#953800;--shiki-dark:#FFA657\">margins_name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</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:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(table.round(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</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>Every argument maps to a spreadsheet pivot: <code dir=\"ltr\">index</code> is the rows, <code dir=\"ltr\">columns</code> the columns, <code dir=\"ltr\">values</code> the field, <code dir=\"ltr\">aggfunc</code> the summary. <code dir=\"ltr\">margins=True</code> adds the totals row and column, and they confirm the result: 446,666 dollars overall, the revenue definition from the last lesson. Revenue grew in every channel from 2024 to 2025, and the web roughly tripled.</p>\n<p><code dir=\"ltr\">pivot_table</code> is the same computation as <code dir=\"ltr\">groupby([&quot;channel&quot;, &quot;year&quot;])[&quot;revenue&quot;].sum().unstack()</code>. Use <code dir=\"ltr\">groupby</code> when you will keep computing, and <code dir=\"ltr\">pivot_table</code> when the grid itself is the output. For counts of distinct orders, change the aggregation: <code dir=\"ltr\">values=&quot;order_id&quot;, aggfunc=&quot;nunique&quot;</code>. And <code dir=\"ltr\">fill_value=0</code> replaces the <code dir=\"ltr\">NaN</code> of empty cells when zero is the honest answer, as it is for “no sales in that cell”.</p>\n<h3 id=\"sec-2\" tabindex=\"-1\">From amounts to shares</h3>\n<p>A grid of amounts invites the next question: how did the <strong>mix</strong> change? Divide each column by its total and the grid shows shares instead. <code dir=\"ltr\">by_year.sum()</code> returns one total per column, and dividing a DataFrame by that Series lines each total up with its own column by label, the same alignment rule you met with two Series in Lesson 2.1. To divide each row by its row total instead, use <code dir=\"ltr\">by_year.div(by_year.sum(axis=1), axis=0)</code>.</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\">sales </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:#1F2328;--shiki-dark:#E6EDF3\">sales </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sales[sales[</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>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">by_year </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sales.pivot_table(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">index</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"channel\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">columns</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">sales[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_date\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].dt.year,</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">                            values</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"revenue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">aggfunc</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"sum\"</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\">((by_year </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> by_year.sum()).round(</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=\"1\" hidden></div></div>\n<p>The web’s share of revenue rose from 53% in 2024 to 59% in 2025, while the app’s fell from 35% to 31%. Amounts tell you everything grew; shares tell you where the growth concentrated. Report both, because each hides what the other shows.</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>pivot versus pivot_table</span></p>\n<p><code dir=\"ltr\">sales.pivot(index=&quot;quarter&quot;, columns=&quot;channel&quot;, values=&quot;revenue&quot;)</code> fails with <code dir=\"ltr\">ValueError: Index contains duplicate entries, cannot reshape</code>. <code dir=\"ltr\">pivot</code> is a pure reshape: it expects exactly one value per cell, and raw data has hundreds. Use <code dir=\"ltr\">pivot_table</code> on raw data, and keep <code dir=\"ltr\">pivot</code> for tables that are already one row per cell.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">crosstab: counts and rates</h2>\n<p>For two categorical columns, <code dir=\"ltr\">pd.crosstab</code> counts combinations, and <code dir=\"ltr\">normalize</code> converts counts to shares. Here is Cartwheel’s web traffic:</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\">sessions </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\">\"web_sessions.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\">(pd.crosstab(sessions[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"source\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], sessions[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"device\"</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\">(pd.crosstab(sessions[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"device\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], sessions[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"purchased\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], </span><span style=\"color:#953800;--shiki-dark:#FFA657\">normalize</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"index\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).round(</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=\"2\" hidden></div></div>\n<p>The second table answers a real question. <code dir=\"ltr\">normalize=&quot;index&quot;</code> makes each row sum to 1, so the <code dir=\"ltr\">1</code> column is the purchase rate per device: 12.4% of desktop sessions end in a purchase, but only 6.7% of mobile sessions, although mobile brings the most traffic. That gap is worth a conversation with whoever owns the mobile checkout. Choose the normalisation by the question: <code dir=\"ltr\">&quot;index&quot;</code> for rates within each row group, <code dir=\"ltr\">&quot;columns&quot;</code> for composition of each column, <code dir=\"ltr\">&quot;all&quot;</code> for shares of the grand total.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">melt: from wide back to long</h2>\n<p>The finance team’s 2025 targets arrive as a spreadsheet, one column per quarter. To compare them with actual revenue you need them long, one row per channel and quarter, so they can be merged:</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\">targets </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.DataFrame({</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"channel\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"marketplace\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"mobile_app\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"web\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"Q1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">15000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">28000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"Q2\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">30000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"Q3\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">6000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">20000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">40000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"Q4\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">9000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">30000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">60000</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:#953800;--shiki-dark:#FFA657\">long</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> targets.melt(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">id_vars</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"channel\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">var_name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"quarter\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">value_name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"target\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">long</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"quarter\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> long</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"quarter\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].str[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].astype(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">int</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:#953800;--shiki-dark:#FFA657\">long</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.head(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">6</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:#953800;--shiki-dark:#FFA657\">long</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.shape)</span></span></code></pre><div class=\"run-output\" data-run-output=\"3\" hidden></div></div>\n<p><code dir=\"ltr\">id_vars</code> are the columns that identify a row and stay as they are; every other column becomes a pair of values in two new columns, the old column name in <code dir=\"ltr\">var_name</code> and its value in <code dir=\"ltr\">value_name</code>. Three rows times four quarters gives twelve. <code dir=\"ltr\">str[1]</code> takes the second character of <code dir=\"ltr\">&quot;Q1&quot;</code>, so the quarter becomes a number that matches <code dir=\"ltr\">sales[&quot;quarter&quot;]</code>, ready for a merge like the ones in Lesson 4.2.</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>Keep data long until the last step</span></p>\n<p>Do your filtering, grouping and merging on long tables, and pivot only to present. Wide tables with years or months as columns are hard to filter (“which column is 2025-Q4?”) and break the moment a new period arrives.</p>\n</aside>\n<p>In the exercise you build the quarterly grid for 2025 and check performance against those targets. Next comes time itself: resampling, periods and growth rates.</p>\n","toc":[{"id":"sec-1","text":"pivot_table: summarise and reshape at once","level":2},{"id":"sec-2","text":"From amounts to shares","level":3},{"id":"sec-3","text":"crosstab: counts and rates","level":2},{"id":"sec-4","text":"melt: from wide back to long","level":2}],"takeaways":["Long data has one row per observation; wide data spreads one variable across columns. pandas analyses long data best, and people read wide tables best.","<code dir=\"ltr\">pivot_table(index=..., columns=..., values=..., aggfunc=&quot;sum&quot;)</code> aggregates and reshapes in one step, like a spreadsheet pivot table.","<code dir=\"ltr\">pivot</code> only reshapes; it raises <code dir=\"ltr\">ValueError: Index contains duplicate entries</code> when several rows share a cell, so use <code dir=\"ltr\">pivot_table</code> for raw data.","<code dir=\"ltr\">pd.crosstab(a, b, normalize=&quot;index&quot;)</code> turns two columns into row percentages, which is the quickest way to compare rates between groups.","<code dir=\"ltr\">melt(id_vars=..., var_name=..., value_name=...)</code> turns wide columns such as <code dir=\"ltr\">Q1</code>…<code dir=\"ltr\">Q4</code> back into rows."],"further":[{"title":"Reshaping and pivot tables","url":"https://pandas.pydata.org/docs/user_guide/reshaping.html"},{"title":"pandas.DataFrame.pivot_table","url":"https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pivot_table.html"},{"title":"pandas.melt","url":"https://pandas.pydata.org/docs/reference/api/pandas.melt.html"}],"quiz":[{"q":"<p><code dir=\"ltr\">sales.pivot(index=&quot;quarter&quot;, columns=&quot;channel&quot;, values=&quot;revenue&quot;)</code> raises <code dir=\"ltr\">ValueError: Index contains duplicate entries, cannot reshape</code>. What is the fix?</p>","options":[{"text":"Sort <code dir=\"ltr\">sales</code> by quarter first.","why":"Order is not the problem; many lines share the same quarter and channel, and <code dir=\"ltr\">pivot</code> has no rule for combining them."},{"text":"Drop duplicate rows with <code dir=\"ltr\">drop_duplicates()</code>.","why":"The rows are different order lines, not duplicates. Dropping them would delete real revenue."},{"text":"Convert <code dir=\"ltr\">quarter</code> to text.","why":"The type of the key is irrelevant; the problem is several values per cell."},{"text":"Use <code dir=\"ltr\">pivot_table</code> with <code dir=\"ltr\">aggfunc=&quot;sum&quot;</code>, which combines the values that land in the same cell.","why":"Correct. <code dir=\"ltr\">pivot_table</code> aggregates first, then reshapes. <code dir=\"ltr\">pivot</code> only works when every index and column pair appears once."}],"answer":3},{"q":"<p>A finance sheet has columns <code dir=\"ltr\">channel, Q1, Q2, Q3, Q4</code>. Which call makes one row per channel and quarter?</p>","options":[{"text":"<code dir=\"ltr\">targets.melt(id_vars=&quot;channel&quot;, var_name=&quot;quarter&quot;, value_name=&quot;target&quot;)</code>","why":"Correct. <code dir=\"ltr\">channel</code> stays as an identifier, and the four quarter columns become rows with their names in <code dir=\"ltr\">quarter</code> and values in <code dir=\"ltr\">target</code>."},{"text":"<code dir=\"ltr\">targets.pivot_table(index=&quot;channel&quot;)</code>","why":"The table is already wide by quarter; pivoting would not move the quarter columns into rows."},{"text":"<code dir=\"ltr\">targets.T</code>","why":"Transposing swaps rows and columns, giving channels as columns, which is still wide."}],"answer":0},{"q":"<p><code dir=\"ltr\">pd.crosstab(sessions[&quot;device&quot;], sessions[&quot;purchased&quot;], normalize=&quot;index&quot;)</code> shows 0.067 in the mobile row, column 1. What does it mean?</p>","options":[{"text":"6.7% of all purchases happened on mobile.","why":"That would be <code dir=\"ltr\">normalize=&quot;columns&quot;</code>. With <code dir=\"ltr\">&quot;index&quot;</code>, each row sums to 1."},{"text":"6.7% of mobile sessions ended in a purchase.","why":"Correct. Normalising by index divides each count by its row total, so the value is the conversion rate within mobile sessions."},{"text":"Mobile has 6.7% of all sessions.","why":"The share of sessions by device would come from <code dir=\"ltr\">value_counts(normalize=True)</code> on <code dir=\"ltr\">device</code>."}],"answer":1}],"exercise":{"kind":"python","files":["order_items.csv","orders.csv"],"starter":"import pandas as pd\n\nitems = pd.read_csv(\"order_items.csv\")\norders = pd.read_csv(\"orders.csv\", parse_dates=[\"order_date\"])\nitems[\"revenue\"] = items[\"quantity\"] * items[\"unit_price\"] * (1 - items[\"discount\"])\nsales = items.merge(orders, on=\"order_id\", how=\"left\", validate=\"many_to_one\")\nsales = sales[sales[\"status\"] != \"cancelled\"]\nsales = sales.assign(year=sales[\"order_date\"].dt.year, quarter=sales[\"order_date\"].dt.quarter)\nsales_2025 = sales[sales[\"year\"] == 2025]\n\ntargets = pd.DataFrame({\n    \"channel\": [\"marketplace\", \"mobile_app\", \"web\"],\n    \"Q1\": [5000, 15000, 28000],\n    \"Q2\": [5000, 16000, 30000],\n    \"Q3\": [6000, 20000, 40000],\n    \"Q4\": [9000, 30000, 60000],\n})\n\n# 1. grid: 2025 revenue with quarters (1-4) as rows and channels as columns\ngrid = None\n\n# 2. targets_long: columns channel, quarter (int 1-4), target - 12 rows\ntargets_long = None\n\n# 3. vs_target: targets_long with an added \"revenue\" column (actual 2025 revenue\n#    for that channel and quarter)\nvs_target = None\n\n# 4. Number of channel-quarters where revenue reached the target\nbeat = 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\"])\nitems[\"revenue\"] = items[\"quantity\"] * items[\"unit_price\"] * (1 - items[\"discount\"])\nsales = items.merge(orders, on=\"order_id\", how=\"left\", validate=\"many_to_one\")\nsales = sales[sales[\"status\"] != \"cancelled\"]\nsales = sales.assign(year=sales[\"order_date\"].dt.year, quarter=sales[\"order_date\"].dt.quarter)\nsales_2025 = sales[sales[\"year\"] == 2025]\n\ntargets = pd.DataFrame({\n    \"channel\": [\"marketplace\", \"mobile_app\", \"web\"],\n    \"Q1\": [5000, 15000, 28000],\n    \"Q2\": [5000, 16000, 30000],\n    \"Q3\": [6000, 20000, 40000],\n    \"Q4\": [9000, 30000, 60000],\n})\n\ngrid = sales_2025.pivot_table(index=\"quarter\", columns=\"channel\", values=\"revenue\", aggfunc=\"sum\")\n\ntargets_long = targets.melt(id_vars=\"channel\", var_name=\"quarter\", value_name=\"target\")\ntargets_long[\"quarter\"] = targets_long[\"quarter\"].str[1].astype(int)\n\nactual = sales_2025.groupby([\"channel\", \"quarter\"], as_index=False)[\"revenue\"].sum()\nvs_target = targets_long.merge(actual, on=[\"channel\", \"quarter\"], how=\"left\", validate=\"one_to_one\")\n\nbeat = int((vs_target[\"revenue\"] >= vs_target[\"target\"]).sum())\nprint(grid.round(0))\nprint(vs_target.round(0))\n","checks":[{"id":"grid","test":"isinstance(grid, pd.DataFrame) and grid.shape == (4, 3) and abs(float(grid.loc[4, 'web']) - 87530) < 1"},{"id":"long","test":"isinstance(targets_long, pd.DataFrame) and len(targets_long) == 12 and {'channel', 'quarter', 'target'} <= set(targets_long.columns) and sorted(set(targets_long['quarter'].tolist())) == [1, 2, 3, 4]"},{"id":"vs-target","test":"isinstance(vs_target, pd.DataFrame) and len(vs_target) == 12 and abs(float(vs_target['revenue'].sum()) - 326808) < 1"},{"id":"beat","test":"int(beat) == 8"}],"title":"2025 against target","prompt":"<p><code dir=\"ltr\">sales_2025</code> holds 2025’s non-cancelled order lines, with <code dir=\"ltr\">quarter</code> and <code dir=\"ltr\">channel</code> columns, and <code dir=\"ltr\">targets</code> is the finance team’s wide sheet. Build <code dir=\"ltr\">grid</code>, a pivot table of revenue with quarters 1 to 4 as rows and channels as columns. Melt <code dir=\"ltr\">targets</code> into <code dir=\"ltr\">targets_long</code> with the columns <code dir=\"ltr\">channel</code>, <code dir=\"ltr\">quarter</code> (an integer 1 to 4) and <code dir=\"ltr\">target</code>. Then make <code dir=\"ltr\">vs_target</code>: <code dir=\"ltr\">targets_long</code> plus a <code dir=\"ltr\">revenue</code> column with the actual revenue for each channel and quarter. Finally set <code dir=\"ltr\">beat</code> to the number of rows where revenue reached the target.</p>\n","hints":["<code dir=\"ltr\">sales_2025.pivot_table(index=&quot;quarter&quot;, columns=&quot;channel&quot;, values=&quot;revenue&quot;, aggfunc=&quot;sum&quot;)</code>.","<code dir=\"ltr\">targets.melt(id_vars=&quot;channel&quot;, var_name=&quot;quarter&quot;, value_name=&quot;target&quot;)</code>, then turn <code dir=\"ltr\">&quot;Q1&quot;</code> into <code dir=\"ltr\">1</code> with <code dir=\"ltr\">.str[1].astype(int)</code>.","Aggregate actuals to the same grain with <code dir=\"ltr\">groupby([&quot;channel&quot;, &quot;quarter&quot;], as_index=False)[&quot;revenue&quot;].sum()</code>, merge on both keys, and count <code dir=\"ltr\">(vs_target[&quot;revenue&quot;] &gt;= vs_target[&quot;target&quot;]).sum()</code>."],"explanation":"<p>The grid is for people; the long table is for computing. Melting the targets and aggregating the actuals to the same grain (one row per channel and quarter) makes the comparison a plain merge on two keys, and <code dir=\"ltr\">validate=&quot;one_to_one&quot;</code> confirms each target met exactly one actual. Cartwheel hit 8 of 12 targets; all four misses came in the first half of the year, and then it beat Q4 everywhere by a wide margin: web Q4 revenue was 87,530 dollars against a 60,000 target. Another clue that something unusual happened in Q4.</p>\n","checkLabels":{"grid":"<code dir=\"ltr\">grid</code> is 4 quarters × 3 channels, with web Q4 at about 87,530","long":"<code dir=\"ltr\">targets_long</code> has 12 rows with integer quarters","vs-target":"<code dir=\"ltr\">vs_target</code> has 12 rows and actual revenue adding up to 2025’s total","beat":"<code dir=\"ltr\">beat</code> is 8"}},"words":707,"readMinutes":3}