{"id":"l-pda-3-3","courseId":"python-data-analysis","lang":"en","kind":"lesson","summary":"Convert money stored as text, such as “$1,234.50”, into a numeric column by stripping symbols, converting with astype or to_numeric, and checking every value that failed to convert.","html":"<p>Ask the messy export for its total sales and you get something strange. No error, no number: a long string starting <code dir=\"ltr\">'$65.0018.0033.00...'</code>. The <code dir=\"ltr\">total</code> column looks numeric in a spreadsheet, but nearly half its values start with a dollar sign, so pandas stored the whole column as text, and adding text joins it. Money arriving as text is one of the most common problems in exported data, and it needs a deliberate, checked fix.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Spot it: dtype object on a number column</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[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].dtype)</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\">\"total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].head(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).tolist())</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\">repr</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(messy[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].sum()[:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">40</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\">\"total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].str.startswith(</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\">\"values start with $\"</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>The inspection routine from Lesson 2.2 catches this immediately: <code dir=\"ltr\">info()</code> would show <code dir=\"ltr\">total</code> as <code dir=\"ltr\">object</code> where you expected <code dir=\"ltr\">float64</code>. Whenever a column that should hold numbers is <code dir=\"ltr\">object</code>, at least one value contains something that is not a number. Here, 76 of 168 values start with <code dir=\"ltr\">$</code>.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Fix it: strip the symbols, then convert</h2>\n<p>Remove every character that is not part of the number, then convert the clean text:</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\">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\">total </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    messy[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    .str.strip()</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    .str.replace(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"$\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">regex</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">False</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    .str.replace(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\",\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">regex</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">False</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    .astype(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">float</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:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(total.dtype, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">round</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(total.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:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(total.describe().round(</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=\"1\" hidden></div></div>\n<p>Wrapping a chain in parentheses lets you put one step per line, which is easier to read and to comment out while debugging. The thousands separator is removed too: this export happens to have no totals above 1,000 dollars, but the next one will, and <code dir=\"ltr\">&quot;1,234.50&quot;</code> would break <code dir=\"ltr\">astype(float)</code>.</p>\n<p><code dir=\"ltr\">describe()</code> is the sanity check. Totals run from 18 to 942.90 dollars with a median of 72, which matches Cartwheel’s catalogue: the cheapest product is a jump rope at 18 dollars, and the largest line is two standing desks. A minimum of 0 or a maximum of 94,290 would tell you a decimal point went missing.</p>\n<p>The 23,500.67 sum is not a revenue figure yet: the export still contains nine duplicate rows. You will remove them in Lesson 3.5.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">The dollar sign trap</h2>\n<p><code dir=\"ltr\">str.replace</code> can also work with regular expressions, a pattern language in which some characters have special meanings. <code dir=\"ltr\">$</code> is one of them: it means “the end of the text”. Watch what happens when you ask for a pattern by accident:</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\">as_pattern </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\">\"total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].str.replace(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"$\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">regex</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">True</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(as_pattern.head(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).tolist())   </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># still '$65.00'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">try</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    pd.to_numeric(as_pattern)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">except</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ValueError</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> error:</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:#0A3069;--shiki-dark:#A5D6FF\">\"ValueError:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, error)</span></span></code></pre><div class=\"run-output\" data-run-output=\"2\" hidden></div></div>\n<p>The pattern <code dir=\"ltr\">$</code> matched the empty position at the end of each string and replaced it with nothing, so every dollar sign survived. In pandas 2, <code dir=\"ltr\">str.replace</code> defaults to <code dir=\"ltr\">regex=False</code>, but older tutorials and older pandas versions defaulted to patterns, and you will meet code written either way. Writing <code dir=\"ltr\">regex=False</code> explicitly makes the intent clear to every reader and every version. If you do want a pattern, escape the special character or use a character class: <code dir=\"ltr\">str.replace(r&quot;[$,]&quot;, &quot;&quot;, regex=True)</code> removes both symbols in one step.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">When some values will not convert</h2>\n<p><code dir=\"ltr\">astype(float)</code> is all or nothing: one bad value and the whole conversion fails. <code dir=\"ltr\">pd.to_numeric</code> with <code dir=\"ltr\">errors=&quot;coerce&quot;</code> converts what it can and turns the rest into <code dir=\"ltr\">NaN</code>. That is useful and dangerous in equal measure, so it always comes with a follow-up check:</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\">raw </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.Series([</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"$1,234.50\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"18.00\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"N/A\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"€89.00\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\" $65.00 \"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">cleaned </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> raw.str.strip().str.replace(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"$\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">regex</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">False</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).str.replace(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\",\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">regex</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">False</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">amount </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.to_numeric(cleaned, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">errors</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"coerce\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(amount)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">failed </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> amount.isna() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> raw.notna()</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:#0A3069;--shiki-dark:#A5D6FF\">\"Could not convert:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, raw[failed].tolist())</span></span></code></pre><div class=\"run-output\" data-run-output=\"3\" hidden></div></div>\n<p>The check lists exactly what failed: <code dir=\"ltr\">N/A</code>, a genuinely missing value, and <code dir=\"ltr\">€89.00</code>, a real sale in a currency your cleaner did not expect. Those need different treatment. The first stays missing; the second needs a conversion rule, or at least a conversation with whoever produced the file. Coercing without looking would have quietly dropped 89 euros of sales from every total.</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>Coercing and moving on</span></p>\n<p><code dir=\"ltr\">pd.to_numeric(col, errors=&quot;coerce&quot;)</code> followed by <code dir=\"ltr\">.sum()</code> always “works”. If a new export switches to European formatting, your cleaner turns <code dir=\"ltr\">1.234,50</code> into 1.2345 and <code dir=\"ltr\">65,00</code> into 6500, anything stranger becomes <code dir=\"ltr\">NaN</code>, and revenue goes wrong without a single error. Count the new <code dir=\"ltr\">NaN</code>s every time, <code dir=\"ltr\">(amount.isna() &amp; raw.notna()).sum()</code> should be a number you can explain, and check that the minimum and maximum still make sense.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">Make it a function</h2>\n<p>You will clean money columns again, in this course and in your job. Put the logic where it can be reused and tested:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"4\"><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=\"4\" 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:#CF222E;--shiki-dark:#FF7B72\">def</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> parse_money</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(values):</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">    \"\"\"Convert text like ' $1,234.50' to floats; unparseable values become NaN.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    cleaned </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        values.astype(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"string\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        .str.strip()</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        .str.replace(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"$\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">regex</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">False</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        .str.replace(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\",\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">regex</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">False</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    )</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd.to_numeric(cleaned, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">errors</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"coerce\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#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\">\"total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> parse_money(messy[</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\">(messy[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].dtype, messy[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"total\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].isna().sum(), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"unparsed\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span></code></pre><div class=\"run-output\" data-run-output=\"4\" hidden></div></div>\n<p><code dir=\"ltr\">astype(&quot;string&quot;)</code> first makes the function safe on columns that pandas already read as numbers (when a file happens to have no <code dir=\"ltr\">$</code> at all), because <code dir=\"ltr\">.str</code> methods only work on text. Because the input is a <code dir=\"ltr\">string</code> column, <code dir=\"ltr\">to_numeric</code> returns the nullable <code dir=\"ltr\">Float64</code> dtype from Lesson 3.1, with <code dir=\"ltr\">&lt;NA&gt;</code> wherever a value failed. The function returns a new Series, and the caller decides where to store it, the same principle as returning rather than printing in Lesson 1.5.</p>\n<aside class=\"callout callout-why\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">Why this matters</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Why this matters</span></p>\n<p>Revenue is the number people quote in meetings. When it is wrong because a dollar sign survived or a euro value vanished, nobody blames pandas; they stop trusting the analysis. Two lines of checking after every conversion are the cheapest insurance in data work.</p>\n</aside>\n<p>In the exercise you build the converter and use it on the export. Next comes the hardest column in the file: dates written three different ways.</p>\n","toc":[{"id":"sec-1","text":"Spot it: dtype object on a number column","level":2},{"id":"sec-2","text":"Fix it: strip the symbols, then convert","level":2},{"id":"sec-3","text":"The dollar sign trap","level":2},{"id":"sec-4","text":"When some values will not convert","level":2},{"id":"sec-5","text":"Make it a function","level":2}],"takeaways":["A numeric-looking column with dtype <code dir=\"ltr\">object</code> is text, and <code dir=\"ltr\">sum()</code> on it concatenates strings instead of adding numbers.","Remove currency symbols and thousands separators with <code dir=\"ltr\">str.replace(..., regex=False)</code>, then convert with <code dir=\"ltr\">astype(float)</code> or <code dir=\"ltr\">pd.to_numeric</code>.","In a regular expression <code dir=\"ltr\">$</code> means “end of text”, so <code dir=\"ltr\">str.replace(&quot;$&quot;, &quot;&quot;, regex=True)</code> removes nothing.","<code dir=\"ltr\">pd.to_numeric(errors=&quot;coerce&quot;)</code> turns unparseable values into <code dir=\"ltr\">NaN</code>; always list the values that failed before moving on.","Wrap the cleaning in a function and sanity-check the result with <code dir=\"ltr\">describe()</code>, the minimum and the maximum."],"further":[{"title":"pandas.to_numeric","url":"https://pandas.pydata.org/docs/reference/api/pandas.to_numeric.html"},{"title":"pandas.Series.astype","url":"https://pandas.pydata.org/docs/reference/api/pandas.Series.astype.html"},{"title":"Regular expression syntax (re module)","url":"https://docs.python.org/3/library/re.html#regular-expression-syntax"}],"quiz":[{"q":"<p><code dir=\"ltr\">messy[&quot;total&quot;]</code> has dtype <code dir=\"ltr\">object</code>. What does <code dir=\"ltr\">messy[&quot;total&quot;].sum()</code> return?</p>","options":[{"text":"The correct total as a float","why":"pandas adds text with <code dir=\"ltr\">+</code>, which joins strings. You only get a numeric total after converting."},{"text":"A <code dir=\"ltr\">TypeError</code>","why":"<code dir=\"ltr\">+</code> is defined for strings, so pandas happily concatenates them with no error, which is what makes this trap dangerous."},{"text":"<code dir=\"ltr\">NaN</code>, because some values contain <code dir=\"ltr\">$</code>","why":"Nothing is parsed, so there is no conversion failure to produce <code dir=\"ltr\">NaN</code>."},{"text":"One long string with every total glued together","why":"Correct. Strings concatenate under <code dir=\"ltr\">+</code>, so the “sum” is text like <code dir=\"ltr\">'$65.0018.0033.00...'</code>."}],"answer":3},{"q":"<p>Which line removes the dollar signs from <code dir=\"ltr\">&quot;$65.00&quot;</code>-style values?</p>","options":[{"text":"<code dir=\"ltr\">s.str.replace(&quot;$&quot;, &quot;&quot;, regex=False)</code>","why":"Correct. With <code dir=\"ltr\">regex=False</code> the <code dir=\"ltr\">$</code> is matched as a literal character. (It is also the default for <code dir=\"ltr\">str.replace</code> in pandas 2.)"},{"text":"<code dir=\"ltr\">s.str.replace(&quot;$&quot;, &quot;&quot;, regex=True)</code>","why":"As a pattern, <code dir=\"ltr\">$</code> matches the end of the string, an empty position, so nothing visible is removed."},{"text":"<code dir=\"ltr\">s.str.strip(&quot;$&quot;)</code> followed by <code dir=\"ltr\">astype(int)</code>","why":"Stripping <code dir=\"ltr\">$</code> works for leading signs, but <code dir=\"ltr\">astype(int)</code> fails on <code dir=\"ltr\">&quot;65.00&quot;</code> because it has decimals."}],"answer":0},{"q":"<p><code dir=\"ltr\">pd.to_numeric(cleaned, errors=&quot;coerce&quot;)</code> returns 4 <code dir=\"ltr\">NaN</code> values that were not missing before. What should you do next?</p>","options":[{"text":"Fill them with 0 so the totals add up.","why":"That hides four real sales. Find out what they were first."},{"text":"Print the original strings for those rows and decide how to handle each format.","why":"Correct. Coercion is a way to find problems, not a way to make them disappear. Those rows may hold a euro sign or a European decimal comma your cleaner missed."},{"text":"Drop those rows, since they are invalid.","why":"They are valid sales in a format you did not expect. Dropping them understates revenue."},{"text":"Switch to <code dir=\"ltr\">errors=&quot;ignore&quot;</code> so the column stays unchanged.","why":"Then nothing is converted at all, and the option is deprecated in recent pandas versions."}],"answer":1}],"exercise":{"kind":"python","files":["orders_messy.csv"],"starter":"import pandas as pd\n\ndef parse_money(values):\n    \"\"\"Convert a Series of text like ' $1,234.50' to floats.\n    Values that cannot be converted become NaN.\"\"\"\n    return values\n\n\nmessy = pd.read_csv(\"orders_messy.csv\")\n\n# 1. Replace messy[\"total\"] with the parsed numbers\n\n# 2. Sum of all totals, rounded to 2 decimals\ntotal_sum = None\n\n# 3. Number of rows with a total above 200\nover_200 = None\n","solution":"import pandas as pd\n\ndef parse_money(values):\n    \"\"\"Convert a Series of text like ' $1,234.50' to floats.\n    Values that cannot be converted become NaN.\"\"\"\n    cleaned = (\n        values.astype(\"string\")\n        .str.strip()\n        .str.replace(\"$\", \"\", regex=False)\n        .str.replace(\",\", \"\", regex=False)\n    )\n    return pd.to_numeric(cleaned, errors=\"coerce\")\n\n\nmessy = pd.read_csv(\"orders_messy.csv\")\n\nmessy[\"total\"] = parse_money(messy[\"total\"])\ntotal_sum = round(float(messy[\"total\"].sum()), 2)\nover_200 = int((messy[\"total\"] > 200).sum())\n","checks":[{"id":"parse-basic","test":"[float(x) for x in parse_money(pd.Series(['$65.00', '18.00', ' $129.00 ']))] == [65.0, 18.0, 129.0]"},{"id":"parse-thousands","test":"float(parse_money(pd.Series(['$1,234.50'])).iloc[0]) == 1234.5"},{"id":"parse-bad","test":"pd.isna(parse_money(pd.Series(['N/A', '$12.00'])).iloc[0])"},{"id":"column-numeric","test":"pd.api.types.is_float_dtype(messy['total']) and int(messy['total'].isna().sum()) == 0"},{"id":"sum","test":"round(float(total_sum), 2) == 23500.67"},{"id":"over-200","test":"int(over_200) == 31"}],"title":"Build a money parser","prompt":"<p>Complete <code dir=\"ltr\">parse_money(values)</code> so it takes a Series of text such as <code dir=\"ltr\">&quot; $1,234.50&quot;</code>, <code dir=\"ltr\">&quot;$65.00&quot;</code> or <code dir=\"ltr\">&quot;18.00&quot;</code> and returns a Series of floats, turning anything unparseable (like <code dir=\"ltr\">&quot;N/A&quot;</code>) into <code dir=\"ltr\">NaN</code> instead of raising an error. Then replace <code dir=\"ltr\">messy[&quot;total&quot;]</code> with the parsed values, set <code dir=\"ltr\">total_sum</code> to their sum rounded to 2 decimals, and set <code dir=\"ltr\">over_200</code> to the number of rows with a total above 200.</p>\n","hints":["Inside the function: strip spaces, remove <code dir=\"ltr\">$</code> and <code dir=\"ltr\">,</code> with <code dir=\"ltr\">.str.replace(..., regex=False)</code>, then convert.","<code dir=\"ltr\">pd.to_numeric(cleaned, errors=&quot;coerce&quot;)</code> converts what it can and returns <code dir=\"ltr\">NaN</code> for the rest.","Assign back with <code dir=\"ltr\">messy[&quot;total&quot;] = parse_money(messy[&quot;total&quot;])</code>; count with <code dir=\"ltr\">(messy[&quot;total&quot;] &gt; 200).sum()</code>."],"explanation":"<p>The function removes the two symbols as plain text and lets <code dir=\"ltr\">to_numeric</code> do the conversion, with <code dir=\"ltr\">errors=&quot;coerce&quot;</code> so a stray <code dir=\"ltr\">N/A</code> becomes <code dir=\"ltr\">NaN</code> rather than crashing the whole column. In this export every value parses (no new <code dir=\"ltr\">NaN</code>s), which is what the <code dir=\"ltr\">column-numeric</code> check confirms. <code dir=\"ltr\">astype(&quot;string&quot;)</code> at the start is optional here but makes the function safe on a column pandas already read as numbers. The sum, 23,500.67, still double-counts nine duplicate rows; you fix that in Lesson 3.5.</p>\n","checkLabels":{"parse-basic":"<code dir=\"ltr\">parse_money</code> handles <code dir=\"ltr\">$</code>, plain numbers and stray spaces","parse-thousands":"<code dir=\"ltr\">parse_money</code> removes thousands separators","parse-bad":"<code dir=\"ltr\">parse_money</code> turns <code dir=\"ltr\">&quot;N/A&quot;</code> into <code dir=\"ltr\">NaN</code> instead of raising","column-numeric":"<code dir=\"ltr\">messy[&quot;total&quot;]</code> is a float column with no failed conversions","sum":"<code dir=\"ltr\">total_sum</code> is 23,500.67","over-200":"31 rows have a total above 200"}},"words":781,"readMinutes":4}