{"id":"l-pda-3-1","courseId":"python-data-analysis","lang":"en","kind":"lesson","summary":"Find missing values with isna, understand which text read_csv silently turns into NaN, and choose deliberately between recovering, labelling, dropping or keeping them with nullable dtypes.","html":"<p>Open <code dir=\"ltr\">orders_messy.csv</code> in a spreadsheet and the gaps are easy to miss: an empty quantity cell here, <code dir=\"ltr\">N/A</code> in the channel column there. Load it into pandas and those gaps become <code dir=\"ltr\">NaN</code>, which then flows silently through your calculations: sums skip it, means ignore it, and a <code dir=\"ltr\">fillna(0)</code> written in a hurry turns “we don’t know” into “nothing was sold”. Missing values are not a technical nuisance to clear away; each one is a question about the business.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Finding the gaps</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"0\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pandas </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">messy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders_messy.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(messy.shape)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(messy.isna().sum())</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(messy[messy.isna().any(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">axis</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)].head().to_string())</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p><code dir=\"ltr\">isna()</code> returns a DataFrame of booleans, and summing it counts the gaps per column: 9 quantities and 7 channels. <code dir=\"ltr\">isna().any(axis=1)</code> asks, per row, whether any column is missing (<code dir=\"ltr\">axis=1</code> means “across the columns”), which lets you look at the actual damaged rows. Looking at them matters: the 9 missing quantities belong to ordinary orders across both years, not to one broken day.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">What read_csv decided for you</h2>\n<p>The raw file has no empty channel cells. It has the text <code dir=\"ltr\">N/A</code>, and pandas converted it to <code dir=\"ltr\">NaN</code> because <code dir=\"ltr\">N/A</code> is on its default list of missing markers, along with an empty cell, <code dir=\"ltr\">NA</code>, <code dir=\"ltr\">NaN</code>, <code dir=\"ltr\">null</code>, <code dir=\"ltr\">None</code> and a few others. Usually that is helpful. Sometimes it is wrong: a column of country codes would lose Namibia, whose ISO code is <code dir=\"ltr\">NA</code>. Load the file without the conversion to see what is really there:</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\">raw </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders_messy.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">keep_default_na</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">False</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(raw[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"channel\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].value_counts())</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((raw[</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:#0A3069;--shiki-dark:#A5D6FF\"> \"\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).sum(), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"empty quantity cells\"</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>Now <code dir=\"ltr\">N/A</code> shows up as a real value with a count of 7, and the quantity column is all text, because the empty cells stayed empty strings. To control the list precisely, pass <code dir=\"ltr\">na_values=[&quot;N/A&quot;, &quot;&quot;]</code> together with <code dir=\"ltr\">keep_default_na=False</code>.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Deciding what to do</h2>\n<p>There is no universal fix. Ask three questions, in this order, and keep the value missing if every answer is no:</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 250\" role=\"img\" aria-labelledby=\"l-pda-3-1-t1\"><defs><marker id=\"arrow-l-pda-3-1-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-pda-3-1-t1\">A decision path. First: can the value be recovered from another source? If yes, recover it. Second: does missing carry meaning, such as no coupon? If yes, label it. Third: is the row useless without it for this analysis? If yes, drop it for that analysis. Otherwise keep it as missing.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"20\" width=\"150\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"85\" y=\"45\" text-anchor=\"middle\">Recoverable from</text>\n  <text class=\"d-label\" x=\"85\" y=\"65\" text-anchor=\"middle\">another source?</text>\n  <rect class=\"d-box\" x=\"185\" y=\"20\" width=\"150\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"260\" y=\"45\" text-anchor=\"middle\">Does \"missing\"</text>\n  <text class=\"d-label\" x=\"260\" y=\"65\" text-anchor=\"middle\">mean something?</text>\n  <rect class=\"d-box\" x=\"360\" y=\"20\" width=\"150\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"435\" y=\"45\" text-anchor=\"middle\">Row useless</text>\n  <text class=\"d-label\" x=\"435\" y=\"65\" text-anchor=\"middle\">without it?</text>\n  <rect class=\"d-box-primary\" x=\"535\" y=\"20\" width=\"150\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"610\" y=\"55\" text-anchor=\"middle\">Keep as NA</text>\n  <path class=\"d-arrow\" d=\"M160 50 L183 50\" marker-end=\"url(#arrow-l-pda-3-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M335 50 L358 50\" marker-end=\"url(#arrow-l-pda-3-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M510 50 L533 50\" marker-end=\"url(#arrow-l-pda-3-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"171\" y=\"40\" text-anchor=\"middle\">no</text>\n  <text class=\"d-label-muted\" x=\"346\" y=\"40\" text-anchor=\"middle\">no</text>\n  <text class=\"d-label-muted\" x=\"521\" y=\"40\" text-anchor=\"middle\">no</text>\n  <path class=\"d-arrow\" d=\"M85 80 L85 140\" marker-end=\"url(#arrow-l-pda-3-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M260 80 L260 140\" marker-end=\"url(#arrow-l-pda-3-1-1)\"/>\n  <path class=\"d-arrow\" d=\"M435 80 L435 140\" marker-end=\"url(#arrow-l-pda-3-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"100\" y=\"115\">yes</text>\n  <text class=\"d-label-muted\" x=\"275\" y=\"115\">yes</text>\n  <text class=\"d-label-muted\" x=\"450\" y=\"115\">yes</text>\n  <rect class=\"d-box-success\" x=\"10\" y=\"144\" width=\"150\" height=\"50\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"85\" y=\"174\" text-anchor=\"middle\">Recover it</text>\n  <rect class=\"d-box-accent\" x=\"185\" y=\"144\" width=\"150\" height=\"50\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"260\" y=\"174\" text-anchor=\"middle\">Label it</text>\n  <rect class=\"d-box-warn\" x=\"360\" y=\"144\" width=\"150\" height=\"50\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"435\" y=\"174\" text-anchor=\"middle\">Drop (for now)</text>\n  <text class=\"d-label-muted\" x=\"85\" y=\"220\" text-anchor=\"middle\">join a clean table</text>\n  <text class=\"d-label-muted\" x=\"260\" y=\"220\" text-anchor=\"middle\">fillna(\"unknown\")</text>\n  <text class=\"d-label-muted\" x=\"435\" y=\"220\" text-anchor=\"middle\">dropna(subset=...)</text>\n</svg><figcaption>Three questions for every missing value</figcaption></figure>\n<p>Recovery comes first because it is the only option that adds information. The export’s order ids also exist in Cartwheel’s clean <code dir=\"ltr\">orders</code> and <code dir=\"ltr\">order_items</code> tables, so the missing quantities could be looked up there; you will learn the join that does it in Lesson 4.2. Until you can recover a value, do not invent one.</p>\n<p>Applied to the export: the missing <strong>channel</strong> cannot be guessed from anything else in the row, but “we don’t know the channel” is still a fact worth keeping visible, so it gets an explicit label. A blank <strong>coupon code</strong> in the clean <code dir=\"ltr\">orders</code> table is different again: there, missing means “no coupon used”, which is a real answer.</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\">messy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders_messy.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">messy[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"channel\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> messy[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"channel\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].fillna(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"unknown\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(messy[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"channel\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].value_counts())</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">len</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(messy.dropna(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">subset</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"quantity\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"rows with a known quantity\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span></code></pre><div class=\"run-output\" data-run-output=\"2\" hidden></div></div>\n<p><code dir=\"ltr\">dropna(subset=[&quot;quantity&quot;])</code> removes only the rows missing a quantity, and only in the result you store; plain <code dir=\"ltr\">dropna()</code> with no subset removes a row if <strong>any</strong> column is missing, which on the raw export would throw away 16 rows, including perfectly good totals. Drop for a specific analysis, not from your master copy.</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>Filling with zero to make the gaps go away</span></p>\n<p>A blank quantity is not zero: every order has at least one item. <code dir=\"ltr\">fillna(0)</code> drops the average quantity from 1.43 to 1.36 and makes nine real orders look empty. Fill only with a value you can defend in a sentence to your manager.</p>\n</aside>\n<p>One more trap: you cannot find gaps with <code dir=\"ltr\">==</code>. <code dir=\"ltr\">NaN</code> is not equal to anything, not even itself, so <code dir=\"ltr\">messy[&quot;channel&quot;] == np.nan</code> is <code dir=\"ltr\">False</code> on every row. Always use <code dir=\"ltr\">isna()</code> and <code dir=\"ltr\">notna()</code>.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Nullable types: integers that can be missing</h2>\n<p>The quantity column loaded as <code dir=\"ltr\">float64</code>, so quantities print as <code dir=\"ltr\">1.0</code> and <code dir=\"ltr\">2.0</code>. That is the same effect you saw with <code dir=\"ltr\">referred_by</code> in Lesson 2.2: classic <code dir=\"ltr\">NaN</code> is a float. pandas also has <strong>nullable</strong> dtypes, written with a capital letter, which store a separate missing marker, <code dir=\"ltr\">pd.NA</code>:</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\">messy </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.read_csv(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"orders_messy.csv\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">qty </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> messy[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"quantity\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].astype(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Int64\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(qty.dtype, qty.iloc[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">32</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">35</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].tolist())</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(qty.sum(), qty.count(), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">len</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(qty))</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\">(qty.mean(), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</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><code dir=\"ltr\">Int64</code> keeps whole numbers as integers and shows gaps as <code dir=\"ltr\">&lt;NA&gt;</code>. Aggregations skip missing values by default, so <code dir=\"ltr\">sum()</code> adds the 159 known quantities, and <code dir=\"ltr\">count()</code> versus <code dir=\"ltr\">len()</code> tells you exactly how many are missing. The same family includes <code dir=\"ltr\">Float64</code>, <code dir=\"ltr\">boolean</code> and <code dir=\"ltr\">string</code>, and <code dir=\"ltr\">pd.read_csv(..., dtype_backend=&quot;numpy_nullable&quot;)</code> loads every column that way. One behaviour to know: comparisons with <code dir=\"ltr\">pd.NA</code> return <code dir=\"ltr\">&lt;NA&gt;</code> rather than <code dir=\"ltr\">False</code>, because “is an unknown quantity equal to 1?” honestly has no answer.</p>\n<p>In the exercise you count, label and retype the gaps. Next you clean the text columns, where the country names come in four different spellings.</p>\n","toc":[{"id":"sec-1","text":"Finding the gaps","level":2},{"id":"sec-2","text":"What read_csv decided for you","level":2},{"id":"sec-3","text":"Deciding what to do","level":2},{"id":"sec-4","text":"Nullable types: integers that can be missing","level":2}],"takeaways":["<code dir=\"ltr\">read_csv</code> turns empty cells and strings like <code dir=\"ltr\">N/A</code>, <code dir=\"ltr\">NA</code> and <code dir=\"ltr\">null</code> into missing values by default, so check the raw file when counts look surprising.","<code dir=\"ltr\">df.isna().sum()</code> counts blanks per column; <code dir=\"ltr\">df[df.isna().any(axis=1)]</code> shows the rows that have any.","Every missing value needs a decision - recover it, label it, drop the row, or keep it missing - and filling with 0 is rarely right.","The nullable <code dir=\"ltr\">Int64</code> dtype keeps whole numbers as integers and shows gaps as <code dir=\"ltr\">&lt;NA&gt;</code>, instead of turning the column into floats.","Sums, means and counts skip missing values by default, so <code dir=\"ltr\">count()</code> and <code dir=\"ltr\">len()</code> differ exactly by the number of gaps."],"further":[{"title":"Working with missing data","url":"https://pandas.pydata.org/docs/user_guide/missing_data.html"},{"title":"Nullable integer data type","url":"https://pandas.pydata.org/docs/user_guide/integer_na.html"},{"title":"pandas.DataFrame.fillna","url":"https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.fillna.html"}],"quiz":[{"q":"<p>The messy export has 9 blank quantities. A colleague runs <code dir=\"ltr\">fillna(0)</code> on the column before computing the average quantity per order. What happens?</p>","options":[{"text":"The average is unchanged, because zeros do not affect a mean.","why":"Zeros count as real values, so they enlarge the denominator and pull the mean down (here from about 1.43 to 1.36)."},{"text":"The average is still correct, because pandas ignores zeros.","why":"pandas ignores missing values in <code dir=\"ltr\">mean()</code>, not zeros. Filling turned “unknown” into “zero”, which is a claim about the data."},{"text":"pandas raises an error because the column is now mixed.","why":"Filling with 0 keeps the column numeric; there is no error, which is exactly why the mistake goes unnoticed."},{"text":"The average drops, because 9 unknown quantities are now counted as orders of zero items.","why":"Correct. Every order has at least one item, so 0 is not a plausible value. Leaving the gaps as missing gives the honest average of the known quantities."}],"answer":3},{"q":"<p>The raw CSV has the text <code dir=\"ltr\">N/A</code> in the channel column for 7 rows. After <code dir=\"ltr\">pd.read_csv</code>, what does <code dir=\"ltr\">value_counts()</code> show for it?</p>","options":[{"text":"A row <code dir=\"ltr\">N/A</code> with count 7","why":"<code dir=\"ltr\">N/A</code> is on pandas’ default list of missing-value markers, so it is converted to <code dir=\"ltr\">NaN</code> and dropped from <code dir=\"ltr\">value_counts()</code>."},{"text":"Nothing; the 7 rows are missing values and are excluded unless you pass <code dir=\"ltr\">dropna=False</code>","why":"Correct. Pass <code dir=\"ltr\">dropna=False</code> to see them, or <code dir=\"ltr\">keep_default_na=False</code> when loading if <code dir=\"ltr\">N/A</code> should stay a real category."},{"text":"An error, because <code dir=\"ltr\">N/A</code> is not a valid channel","why":"<code dir=\"ltr\">read_csv</code> does not validate categories; it either keeps text or converts known missing markers."}],"answer":1},{"q":"<p><code dir=\"ltr\">q = messy[&quot;quantity&quot;].astype(&quot;Int64&quot;)</code>. What do <code dir=\"ltr\">q.count()</code> and <code dir=\"ltr\">len(q)</code> return for 168 rows with 9 gaps?</p>","options":[{"text":"<code dir=\"ltr\">159</code> and <code dir=\"ltr\">168</code>","why":"Correct. <code dir=\"ltr\">count()</code> counts non-missing values; <code dir=\"ltr\">len()</code> counts rows, gaps included."},{"text":"<code dir=\"ltr\">168</code> and <code dir=\"ltr\">168</code>","why":"<code dir=\"ltr\">count()</code> never counts missing values, whatever the dtype."},{"text":"<code dir=\"ltr\">159</code> and <code dir=\"ltr\">159</code>","why":"Missing values are still rows; <code dir=\"ltr\">len()</code> includes them."}],"answer":0}],"exercise":{"kind":"python","files":["orders_messy.csv"],"starter":"import pandas as pd\n\nmessy = pd.read_csv(\"orders_messy.csv\")\n\n# 1. A Series: number of missing values in each column\nmissing_by_col = None\n\n# 2. Replace missing channels with the label \"unknown\" (in messy itself)\n\n# 3. Convert quantity to the nullable integer dtype \"Int64\" (in messy itself)\n\n# 4. Average quantity of the rows where it is known, rounded to 2 decimals\navg_quantity = None\n","solution":"import pandas as pd\n\nmessy = pd.read_csv(\"orders_messy.csv\")\n\nmissing_by_col = messy.isna().sum()\nprint(missing_by_col)\n\nmessy[\"channel\"] = messy[\"channel\"].fillna(\"unknown\")\nmessy[\"quantity\"] = messy[\"quantity\"].astype(\"Int64\")\n\navg_quantity = round(float(messy[\"quantity\"].mean()), 2)\n","checks":[{"id":"missing-counts","test":"isinstance(missing_by_col, pd.Series) and int(missing_by_col['quantity']) == 9 and int(missing_by_col['channel']) == 7 and int(missing_by_col.sum()) == 16"},{"id":"channel-labelled","test":"messy['channel'].isna().sum() == 0 and (messy['channel'] == 'unknown').sum() == 7"},{"id":"quantity-int64","test":"str(messy['quantity'].dtype) == 'Int64' and int(messy['quantity'].isna().sum()) == 9"},{"id":"avg","test":"round(float(avg_quantity), 2) == 1.43"},{"id":"rows-kept","test":"len(messy) == 168"}],"title":"Count, label and retype the gaps","prompt":"<p>Load the messy export (done for you). Set <code dir=\"ltr\">missing_by_col</code> to a Series counting missing values per column. In <code dir=\"ltr\">messy</code> itself, replace missing channels with <code dir=\"ltr\">&quot;unknown&quot;</code> and convert <code dir=\"ltr\">quantity</code> to the nullable <code dir=\"ltr\">&quot;Int64&quot;</code> dtype, keeping its gaps as missing. Then set <code dir=\"ltr\">avg_quantity</code> to the mean of the known quantities, rounded to 2 decimals. Keep all 168 rows: no dropping, and no filling quantities.</p>\n","hints":["<code dir=\"ltr\">messy.isna().sum()</code> returns one count per column.","Assign the results back: <code dir=\"ltr\">messy[&quot;channel&quot;] = messy[&quot;channel&quot;].fillna(&quot;unknown&quot;)</code> and <code dir=\"ltr\">messy[&quot;quantity&quot;] = messy[&quot;quantity&quot;].astype(&quot;Int64&quot;)</code>.","<code dir=\"ltr\">mean()</code> skips missing values on its own: <code dir=\"ltr\">round(float(messy[&quot;quantity&quot;].mean()), 2)</code>."],"explanation":"<p>The channel gaps get an explicit label because “unknown channel” is useful information in a report, while the quantity gaps stay missing because, until you recover them with the join from Lesson 4.2, no defensible value exists. <code dir=\"ltr\">Int64</code> (capital I) keeps the column as whole numbers with <code dir=\"ltr\">&lt;NA&gt;</code> for the 9 gaps, instead of the <code dir=\"ltr\">1.0</code>, <code dir=\"ltr\">2.0</code> floats you started with. <code dir=\"ltr\">mean()</code> skips missing values by default, so the average of 1.43 describes the 159 known quantities; filling with 0 first would have produced 1.36, a number that describes no real order.</p>\n","checkLabels":{"missing-counts":"<code dir=\"ltr\">missing_by_col</code> finds 9 missing quantities and 7 missing channels","channel-labelled":"Missing channels are now <code dir=\"ltr\">&quot;unknown&quot;</code>","quantity-int64":"<code dir=\"ltr\">quantity</code> is <code dir=\"ltr\">Int64</code> and still has its 9 gaps","avg":"<code dir=\"ltr\">avg_quantity</code> is 1.43","rows-kept":"All 168 rows are still there"}},"words":710,"readMinutes":3}