{"id":"l-pda-5-4","courseId":"python-data-analysis","lang":"en","kind":"wrapup","summary":"Turn a one-off analysis into a reproducible one with parameters, functions, assertions, saved outputs and a clean top-to-bottom notebook, and see where to go after this course.","html":"<p>The Q4 answer is written. In three months the head of growth will ask the same question about Q1, and the person answering might be you, a colleague, or you having forgotten every detail. An analysis that only works in the notebook where it was born is a one-off. One that anyone can rerun from the raw files and get the same numbers is an asset. The difference is a handful of habits.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The shape of a rerunnable analysis</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:#6E7781;--shiki-dark:#8B949E\"># Parameters: everything that changes between runs lives here.</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">ORDERS_FILE</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"orders.csv\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">ITEMS_FILE</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"order_items.csv\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">QUARTER</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"2025Q4\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">EXCLUDE_STATUSES</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"cancelled\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">def</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> load_orders</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(orders_file, items_file):</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"\"\"One row per order with revenue (after discounts, no shipping).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    items </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(items_file)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    orders </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(orders_file, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">parse_dates</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_date\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    items[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"revenue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> items[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"quantity\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> items[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"unit_price\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> -</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> items[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"discount\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    revenue </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> items.groupby(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">as_index</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">False</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"revenue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].sum()</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    df </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders.merge(revenue, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">on</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">how</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"left\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">validate</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"one_to_one\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    assert</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].is_unique, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"duplicate orders\"</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    assert</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"revenue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].notna().all(), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders without items\"</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">def</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> quarter_summary</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(df, quarter, exclude):</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    kept </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> df[</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">~</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">df[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"status\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].isin(exclude)]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    in_q </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> kept[kept[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"order_date\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].dt.to_period(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Q\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">==</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> quarter]</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    assert</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> len</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(in_q) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"no orders in </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">quarter</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">        \"quarter\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: quarter,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">        \"revenue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">round</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(in_q[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"revenue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].sum(), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">        \"orders\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">len</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(in_q),</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">        \"customers\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: in_q[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"customer_id\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].nunique(),</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">        \"aov\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">round</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(in_q[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"revenue\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].mean(), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">df </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> load_orders(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">ORDERS_FILE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">ITEMS_FILE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">summary </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.DataFrame([quarter_summary(df, q, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">EXCLUDE_STATUSES</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> q </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"2025Q3\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">QUARTER</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]])</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(summary)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"pandas </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">pd.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">__version__</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>Four ideas carry it. <strong>Parameters</strong> at the top name every input and every decision, including the revenue definition, so next quarter’s run changes one line. <strong>Functions</strong> hold the steps, take their inputs as arguments and return results, so they never depend on a variable left over from somewhere else. <strong>Assertions</strong> state what must be true of the data before anything is computed from it, as you did in Lesson 3.5. And the <strong>version print</strong> records which pandas produced the numbers, because defaults do change between major versions.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Notebook hygiene</h2>\n<p>Notebooks are excellent for exploring and dangerous for reporting, because cells can run in any order and variables outlive the cells that made them. Before you share one:</p>\n<ul>\n<li><strong>Restart and Run All.</strong> If it fails or the numbers change, the notebook depended on hidden state.</li>\n<li><strong>Read files, never edit them.</strong> Keep raw exports untouched in a <code dir=\"ltr\">data/raw</code> folder; cleaning happens in code, so it can be repeated.</li>\n<li><strong>Move reusable code out.</strong> Once two notebooks need <code dir=\"ltr\">clean_orders</code>, it belongs in a <code dir=\"ltr\">.py</code> file you import.</li>\n<li><strong>Write outputs to files.</strong> <code dir=\"ltr\">summary.to_csv(&quot;q4_summary.csv&quot;, index=False)</code> produces the numbers the slide uses; copying from the screen invites typos.</li>\n<li><strong>Write the definition next to the number.</strong> A short Markdown cell saying what “revenue” includes saves an argument later.</li>\n</ul>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Chains that read like the recipe</span></p>\n<p><code dir=\"ltr\">df.pipe(clean).pipe(add_revenue).pipe(summarise)</code> runs your functions in order, each receiving the previous result. It reads like the list of steps you would explain to a colleague, and each step can be tested on its own.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">What you can do now</h2>\n<p>You started this course unable to write a line of Python. You can now load and inspect any CSV, clean real-world mess (blanks, typos, money as text, ambiguous dates, duplicates), join and reshape tables, summarise them by group and over time, describe distributions honestly, and turn all of it into an answer with evidence and caveats. That is the daily work of a data analyst.</p>\n<p>Where to go next depends on the questions you want to answer. <a href=\"/courses/sql-for-analysts\">SQL for Analysts</a> uses the same Cartwheel data in a database, which is where most company data actually lives. The <a href=\"/courses/ml-crash-course\">Machine Learning Crash Course</a> picks up <code dir=\"ltr\">churn.csv</code> and asks the next question: which customers are about to leave? Whichever you choose, keep the habits from this course: check before you trust, write the definition down, and make it rerunnable.</p>\n","toc":[{"id":"sec-1","text":"The shape of a rerunnable analysis","level":2},{"id":"sec-2","text":"Notebook hygiene","level":2},{"id":"sec-3","text":"What you can do now","level":2}],"takeaways":["A reproducible analysis gives the same result when someone else runs it from the raw files, top to bottom, with no manual steps.","Put inputs and choices in named parameters at the top (file paths, the quarter, the revenue definition), so rerunning for a new period means changing one line.","Functions hold the steps you repeat; assertions state what the data must look like before results are trusted.","Use Restart and Run All before sharing a notebook, and save results with <code dir=\"ltr\">to_csv(..., index=False)</code> instead of copying numbers by hand."],"further":[{"title":"pandas.DataFrame.to_csv","url":"https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_csv.html"},{"title":"Python Tutorial - Modules","url":"https://docs.python.org/3/tutorial/modules.html"},{"title":"pandas.DataFrame.pipe","url":"https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pipe.html"}],"quiz":[{"q":"<p>A notebook gives the right Q4 number on your laptop, but a colleague gets a different one from the same file. What is the most likely cause?</p>","options":[{"text":"pandas gives slightly different answers on different computers.","why":"pandas is deterministic; the same code on the same data gives the same result, up to tiny float noise."},{"text":"The notebook depends on cells run out of order or on a variable created in a cell that was later deleted.","why":"Correct. Hidden notebook state is the classic cause. Restart and Run All before sharing catches it."},{"text":"The colleague’s screen rounds numbers differently.","why":"Display rounding changes how a number looks, not which number is computed."},{"text":"CSV files change when they are copied.","why":"Copying does not alter a file’s contents; a different result comes from different code paths or different files."}],"answer":1},{"q":"<p>Next quarter you need the same report for 2026Q1. In a well-structured analysis, what should you have to change?</p>","options":[{"text":"Every cell that mentions 2025Q4","why":"Hunting through cells for hard-coded values is exactly what parameters are meant to avoid, and you will miss one."},{"text":"The cleaning functions","why":"Cleaning rules do not depend on the quarter; changing them for each report would make results incomparable."},{"text":"One parameter at the top, such as <code dir=\"ltr\">QUARTER = &quot;2026Q1&quot;</code>, and the input file path if it changed","why":"Correct. Everything downstream reads those names, so the whole analysis follows."}],"answer":2},{"q":"<p>What does an assertion like <code dir=\"ltr\">assert df[&quot;order_id&quot;].is_unique</code> add to a reproducible analysis?</p>","options":[{"text":"It makes the analysis stop with a clear message when new data breaks an assumption the results depend on.","why":"Correct. Without it, a duplicated export would quietly inflate revenue in next quarter’s report."},{"text":"It removes duplicate order ids automatically.","why":"Assertions only check; cleaning is done by code such as <code dir=\"ltr\">drop_duplicates()</code>."},{"text":"It speeds up the groupby that follows.","why":"Assertions add a small amount of work; their value is safety, not speed."}],"answer":0}],"exercise":{"kind":"python","files":["order_items.csv","orders.csv"],"starter":"import pandas as pd\n\nORDERS_FILE = \"orders.csv\"\nITEMS_FILE = \"order_items.csv\"\nQUARTERS = [\"2024Q4\", \"2025Q3\", \"2025Q4\"]\n\n\ndef load_orders(orders_file, items_file):\n    items = pd.read_csv(items_file)\n    orders = pd.read_csv(orders_file, parse_dates=[\"order_date\"])\n    items[\"revenue\"] = items[\"quantity\"] * items[\"unit_price\"] * (1 - items[\"discount\"])\n    revenue = items.groupby(\"order_id\", as_index=False)[\"revenue\"].sum()\n    return orders.merge(revenue, on=\"order_id\", how=\"left\", validate=\"one_to_one\")\n\n\ndef quarter_summary(df, quarter):\n    \"\"\"Return a dict with keys quarter, revenue, orders, aov for one quarter\n    (non-cancelled orders only), revenue and aov rounded to 2 decimals.\n    Assert that the quarter has at least one order.\"\"\"\n    return {}\n\n\ndf = load_orders(ORDERS_FILE, ITEMS_FILE)\n\n# summary: a DataFrame with one row per quarter in QUARTERS, built with quarter_summary\nsummary = None\n\n# Save summary to \"quarter_summary.csv\" without the index\n","solution":"import pandas as pd\n\nORDERS_FILE = \"orders.csv\"\nITEMS_FILE = \"order_items.csv\"\nQUARTERS = [\"2024Q4\", \"2025Q3\", \"2025Q4\"]\n\n\ndef load_orders(orders_file, items_file):\n    items = pd.read_csv(items_file)\n    orders = pd.read_csv(orders_file, parse_dates=[\"order_date\"])\n    items[\"revenue\"] = items[\"quantity\"] * items[\"unit_price\"] * (1 - items[\"discount\"])\n    revenue = items.groupby(\"order_id\", as_index=False)[\"revenue\"].sum()\n    return orders.merge(revenue, on=\"order_id\", how=\"left\", validate=\"one_to_one\")\n\n\ndef quarter_summary(df, quarter):\n    \"\"\"Return a dict with keys quarter, revenue, orders, aov for one quarter\n    (non-cancelled orders only), revenue and aov rounded to 2 decimals.\n    Assert that the quarter has at least one order.\"\"\"\n    kept = df[df[\"status\"] != \"cancelled\"]\n    in_q = kept[kept[\"order_date\"].dt.to_period(\"Q\") == quarter]\n    assert len(in_q) > 0, f\"no orders in {quarter}\"\n    return {\n        \"quarter\": quarter,\n        \"revenue\": round(in_q[\"revenue\"].sum(), 2),\n        \"orders\": len(in_q),\n        \"aov\": round(in_q[\"revenue\"].mean(), 2),\n    }\n\n\ndf = load_orders(ORDERS_FILE, ITEMS_FILE)\n\nsummary = pd.DataFrame([quarter_summary(df, q) for q in QUARTERS])\nsummary.to_csv(\"quarter_summary.csv\", index=False)\nprint(summary)\n","checks":[{"id":"q4","test":"(lambda r: r['quarter'] == '2025Q4' and abs(float(r['revenue']) - 146825.44) < 0.01 and int(r['orders']) == 642 and abs(float(r['aov']) - 228.70) < 0.01)(quarter_summary(df, '2025Q4'))"},{"id":"other-quarter","test":"(lambda r: int(r['orders']) == 228 and abs(float(r['revenue']) - 52537.10) < 0.01)(quarter_summary(df, '2024Q4'))"},{"id":"uses-argument","test":"int(quarter_summary(df[df['order_date'] < '2025-12-01'], '2025Q4')['orders']) == 457"},{"id":"summary","test":"isinstance(summary, pd.DataFrame) and list(summary['quarter']) == QUARTERS and {'revenue', 'orders', 'aov'} <= set(summary.columns)"},{"id":"saved","test":"__import__('os').path.exists('quarter_summary.csv') and len(pd.read_csv('quarter_summary.csv')) == 3 and 'Unnamed: 0' not in pd.read_csv('quarter_summary.csv').columns"}],"title":"Make the quarterly report rerunnable","prompt":"<p>Complete <code dir=\"ltr\">quarter_summary(df, quarter)</code> so it returns a dictionary with the keys <code dir=\"ltr\">quarter</code>, <code dir=\"ltr\">revenue</code>, <code dir=\"ltr\">orders</code> and <code dir=\"ltr\">aov</code> for the given quarter (a string like <code dir=\"ltr\">&quot;2025Q4&quot;</code>), using non-cancelled orders only, with <code dir=\"ltr\">revenue</code> and <code dir=\"ltr\">aov</code> rounded to 2 decimals. It must use its <code dir=\"ltr\">df</code> argument, not a global, and <code dir=\"ltr\">assert</code> that the quarter has at least one order. Then build <code dir=\"ltr\">summary</code>, a DataFrame with one row per quarter in <code dir=\"ltr\">QUARTERS</code>, and save it to <code dir=\"ltr\">quarter_summary.csv</code> without the index.</p>\n","hints":["Filter twice inside the function: drop cancelled orders, then keep rows where <code dir=\"ltr\">df[&quot;order_date&quot;].dt.to_period(&quot;Q&quot;) == quarter</code>.","Return a dict: <code dir=\"ltr\">{&quot;quarter&quot;: quarter, &quot;revenue&quot;: round(in_q[&quot;revenue&quot;].sum(), 2), &quot;orders&quot;: len(in_q), &quot;aov&quot;: round(in_q[&quot;revenue&quot;].mean(), 2)}</code>.","<code dir=\"ltr\">pd.DataFrame([quarter_summary(df, q) for q in QUARTERS])</code> turns a list of dicts into a table (the list-of-dicts shape from Lesson 1.3). Save with <code dir=\"ltr\">summary.to_csv(&quot;quarter_summary.csv&quot;, index=False)</code>."],"explanation":"<p>The function takes everything it needs as arguments, so the check that passes it a filtered table (orders before December only) gets a different, correct answer: 457 orders for a partial Q4. That is what makes a function safe to reuse. Comparing a <code dir=\"ltr\">Period</code> column with a string like <code dir=\"ltr\">&quot;2025Q4&quot;</code> works because pandas converts the string to a period. A list of dictionaries becomes a DataFrame directly, one row per dict, which closes the loop with the very first data structure you met in this course. <code dir=\"ltr\">index=False</code> keeps the file clean: without it, the CSV gets an unnamed extra column of row numbers.</p>\n","checkLabels":{"q4":"<code dir=\"ltr\">quarter_summary(df, &quot;2025Q4&quot;)</code> returns revenue 146,825.44, 642 orders and AOV 228.70","other-quarter":"It works for other quarters too, such as 2024Q4","uses-argument":"It uses the <code dir=\"ltr\">df</code> it is given, not a global table","summary":"<code dir=\"ltr\">summary</code> has one row per quarter in <code dir=\"ltr\">QUARTERS</code>","saved":"<code dir=\"ltr\">quarter_summary.csv</code> is saved with 3 rows and no index column"}},"words":474,"readMinutes":3}