{"id":"l-pda-3-2","courseId":"python-data-analysis","lang":"en","kind":"lesson","summary":"Clean text columns with the .str accessor - trim spaces, standardise case, replace characters, split names and search for patterns - and verify the result against the values you expect.","html":"<p>Ask the messy export how many countries Cartwheel sells to, and it says 22. The real answer is eight. The other fourteen are the same countries typed as <code dir=\"ltr\">UNITED KINGDOM</code>, <code dir=\"ltr\">united kingdom</code> and so on. Group revenue by country now and the UK’s sales are split across three rows, each one too small. Text cleaning is unglamorous, and it is the difference between a right and a wrong chart.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The .str accessor</h2>\n<p>A Series of text has a <code dir=\"ltr\">.str</code> attribute that exposes Python’s string methods, applied to every value at once:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"python\" data-run=\"python\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Python</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"python\" data-index=\"0\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pandas </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pd</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">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\">\"country\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].nunique(), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"spellings\"</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\">\"country\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].value_counts().tail(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">6</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\">clean </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\">\"country\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].str.strip().str.title()</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(clean.nunique(), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"countries\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">sorted</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(clean.unique()))</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>Each <code dir=\"ltr\">.str</code> call returns a new Series, so you chain them: strip the spaces, then normalise the case. 22 spellings become 8, which matches the eight countries Cartwheel actually sells to. Missing values pass through as missing instead of raising an error, which is the main advantage over writing a loop that calls <code dir=\"ltr\">.strip()</code> on each value yourself. Counting distinct values before and after, as the first and third prints do, is the quickest proof that a clean did what you meant.</p>\n<p><code dir=\"ltr\">title()</code> works here because every Cartwheel country is a name made of ordinary words. It would damage values like <code dir=\"ltr\">USA</code> (to <code dir=\"ltr\">Usa</code>) or <code dir=\"ltr\">McDonald</code> (to <code dir=\"ltr\">Mcdonald</code>). When a column has such values, use <code dir=\"ltr\">str.lower()</code> for comparison or a mapping dictionary with <code dir=\"ltr\">map()</code>, as in Lesson 2.5.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Invisible spaces</h2>\n<p>Thirteen customer names in the export have spaces before or after them. They look identical in a table, so the only way to see them is to measure:</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\">padded </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\">\"customer\"</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\">\"customer\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].str.strip()</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(padded.sum(), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"padded names\"</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.loc[padded, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"customer\"</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\">).map(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">repr</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).tolist())</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">messy[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"customer\"</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\">\"customer\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].str.strip()</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p><code dir=\"ltr\">repr()</code> shows a string with its quotes, which makes the spaces visible: <code dir=\"ltr\">'  Hassan Hughes '</code>. Untrimmed names break exactly the operations you rely on later: grouping, matching against another table, removing duplicates. Strip every text column you are going to group or join on, even if it looks clean. <code dir=\"ltr\">str.strip()</code> removes more than ordinary spaces: tabs, line breaks and the non-breaking spaces that text copied from web pages and PDFs often carries are all whitespace to Python, so one call handles them all.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Replace, split, search</h2>\n<p>The export writes the channel as <code dir=\"ltr\">mobile app</code>, while Cartwheel’s clean tables use <code dir=\"ltr\">mobile_app</code>. Matching conventions now saves a failed join in Section 4:</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\">].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>\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 style=\"color:#953800;--shiki-dark:#FFA657\">dropna</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>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">names </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\">\"customer\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].str.strip().str.split(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\" \"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">expand</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:#1F2328;--shiki-dark:#E6EDF3\">names.columns </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"first_name\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"last_name\"</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\">(names.head(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</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\">coffee </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\">\"product\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].str.contains(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"coffee\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">case</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\">(coffee.sum(), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"coffee lines\"</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\">str.split(&quot; &quot;, expand=True)</code> returns a DataFrame with one column per piece; without <code dir=\"ltr\">expand</code>, you get a Series of lists, and <code dir=\"ltr\">.str[0]</code> picks the first piece of each. <code dir=\"ltr\">str.contains()</code> returns a boolean mask, ready for filtering as in Lesson 2.4, and <code dir=\"ltr\">case=False</code> ignores capitals.</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>Dot access on a column called product</span></p>\n<p><code dir=\"ltr\">messy.product.str.contains(&quot;Coffee&quot;)</code> fails with <code dir=\"ltr\">AttributeError: 'function' object has no attribute 'str'</code>, because <code dir=\"ltr\">product</code> is also the name of a DataFrame method that multiplies values. This is the shadowing problem from Lesson 2.3 in the wild. Square brackets, <code dir=\"ltr\">messy[&quot;product&quot;]</code>, always reach the column.</p>\n</aside>\n<h3 id=\"sec-4\" tabindex=\"-1\">Plain text or pattern?</h3>\n<p><code dir=\"ltr\">str.replace</code> and <code dir=\"ltr\">str.contains</code> can also take <strong>regular expressions</strong>, a mini-language for text patterns. In pandas 2, <code dir=\"ltr\">str.replace</code> treats the first argument as plain text unless you pass <code dir=\"ltr\">regex=True</code>, and <code dir=\"ltr\">str.contains</code> treats it as a pattern unless you pass <code dir=\"ltr\">regex=False</code>. The difference bites with symbols that have special meaning in patterns, such as <code dir=\"ltr\">$</code>, <code dir=\"ltr\">.</code> and <code dir=\"ltr\">(</code>. Lesson 3.3 shows the <code dir=\"ltr\">$</code> case, which affects every money column you will ever clean.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Prove the column is clean</h2>\n<p>Cleaning code that worked on this month’s file may meet a new spelling next month. End with a check that states what clean means:</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:#0550AE;--shiki-dark:#79C0FF\">EXPECTED</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Egypt\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"United Arab Emirates\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Saudi Arabia\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Jordan\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">            \"United Kingdom\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Germany\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"United States\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Canada\"</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\">\"country\"</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\">\"country\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].str.strip().str.title()</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">unexpected </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> set</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(messy[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"country\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> EXPECTED</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">assert</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> not</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> unexpected, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">f</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Unknown countries: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">unexpected</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:#0550AE;--shiki-dark:#79C0FF\">print</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"country column OK:\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, messy[</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"country\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].nunique(), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"values\"</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\">assert condition, message</code> does nothing when the condition is true and stops the script with your message when it is false. Set subtraction lists any cleaned value that is not on the expected list, and the f-string puts them in the error so you know exactly what to fix.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>The category dtype</span></p>\n<p>A text column with a few repeated values, such as country or channel, can be stored as <code dir=\"ltr\">astype(&quot;category&quot;)</code>. It uses less memory and records the allowed values. It is optional for a table of 168 rows; reach for it when a file has millions of rows, and pass <code dir=\"ltr\">observed=True</code> when you group by it.</p>\n</aside>\n<p>In the exercise you clean all three text columns of the export. Next come the totals: numbers trapped inside strings like <code dir=\"ltr\">$1,234.50</code>.</p>\n","toc":[{"id":"sec-1","text":"The .str accessor","level":2},{"id":"sec-2","text":"Invisible spaces","level":2},{"id":"sec-3","text":"Replace, split, search","level":2},{"id":"sec-4","text":"Plain text or pattern?","level":3},{"id":"sec-5","text":"Prove the column is clean","level":2}],"takeaways":["The <code dir=\"ltr\">.str</code> accessor applies a string method to every value in a column, and missing values stay missing instead of raising errors.","<code dir=\"ltr\">str.strip()</code> and a consistent case (<code dir=\"ltr\">str.title()</code>, <code dir=\"ltr\">str.lower()</code>) fix most spelling variants; check them with <code dir=\"ltr\">nunique()</code> before and after.","<code dir=\"ltr\">str.replace(old, new)</code> treats <code dir=\"ltr\">old</code> as plain text by default in pandas 2; pass <code dir=\"ltr\">regex=True</code> only when you mean a pattern.","<code dir=\"ltr\">str.split(&quot; &quot;, expand=True)</code> turns one text column into several, and <code dir=\"ltr\">str.contains()</code> builds a boolean mask from a pattern.","Finish every text clean with a check against the list of values you expect, so a new spelling next month fails loudly."],"further":[{"title":"Working with text data","url":"https://pandas.pydata.org/docs/user_guide/text.html"},{"title":"pandas.Series.str.replace","url":"https://pandas.pydata.org/docs/reference/api/pandas.Series.str.replace.html"},{"title":"pandas.Series.str.split","url":"https://pandas.pydata.org/docs/reference/api/pandas.Series.str.split.html"}],"quiz":[{"q":"<p><code dir=\"ltr\">messy[&quot;country&quot;]</code> has 22 distinct values for 8 real countries. Which line is the fix you should write?</p>","options":[{"text":"<code dir=\"ltr\">messy[&quot;country&quot;].str.title()</code>","why":"It happens to give 8 on this file, because its country variants differ only in case. But a value with a stray space in next month’s export would stay distinct from its trimmed twin, so strip first, as you would for any column you group on."},{"text":"<code dir=\"ltr\">messy[&quot;country&quot;].str.strip().str.title()</code>","why":"Correct. Trimming removes invisible differences, and title case maps <code dir=\"ltr\">EGYPT</code>, <code dir=\"ltr\">egypt</code> and <code dir=\"ltr\">Egypt</code> to the same value."},{"text":"<code dir=\"ltr\">messy[&quot;country&quot;].unique()</code>","why":"This lists the 22 variants; it does not change any of them."},{"text":"<code dir=\"ltr\">messy[&quot;country&quot;].str.replace(&quot; &quot;, &quot;&quot;)</code>","why":"This deletes the spaces inside names, turning <code dir=\"ltr\">United Kingdom</code> into <code dir=\"ltr\">UnitedKingdom</code>, and leaves the case variants untouched."}],"answer":1},{"q":"<p>Why does <code dir=\"ltr\">messy.product.str.contains(&quot;Coffee&quot;)</code> raise <code dir=\"ltr\">AttributeError: 'function' object has no attribute 'str'</code>?</p>","options":[{"text":"<code dir=\"ltr\">product</code> is the name of a DataFrame method, so dot access returns the method instead of the column.","why":"Correct. <code dir=\"ltr\">DataFrame.product()</code> multiplies values, and it shadows the column. <code dir=\"ltr\">messy[&quot;product&quot;]</code> always reaches the column."},{"text":"The column contains missing values.","why":"Missing values make <code dir=\"ltr\">.str</code> methods return <code dir=\"ltr\">NaN</code> for those rows; they do not cause an <code dir=\"ltr\">AttributeError</code>."},{"text":"<code dir=\"ltr\">contains</code> only works on columns that were loaded with <code dir=\"ltr\">dtype=&quot;string&quot;</code>.","why":"<code dir=\"ltr\">.str.contains</code> works on ordinary text (object) columns too."}],"answer":0},{"q":"<p>You clean the country column and then run <code dir=\"ltr\">assert set(clean) &lt;= set(EXPECTED)</code>. What is the assertion for?</p>","options":[{"text":"It speeds up later groupby operations.","why":"An assertion only checks a condition; it does not change data or performance."},{"text":"It sorts the countries alphabetically.","why":"Sets have no order, and nothing is changed by the check."},{"text":"It stops the script if any cleaned value is not one of the known countries, such as a new misspelling next month.","why":"Correct. A failing assertion is far better than a report with a ninth “country” quietly holding some of the revenue."}],"answer":2}],"exercise":{"kind":"python","files":["orders_messy.csv"],"starter":"import pandas as pd\n\nmessy = pd.read_csv(\"orders_messy.csv\")\n\n# 1. customer: remove leading/trailing spaces\n\n# 2. country: remove spaces and use title case (\"UNITED KINGDOM\" -> \"United Kingdom\")\n\n# 3. channel: \"mobile app\" -> \"mobile_app\", and missing -> \"unknown\"\n\n# 4. New column first_name: the first word of the cleaned customer name\n\n# 5. Number of rows whose product name mentions \"desk\" (any capitalisation)\ndesk_lines = None\n","solution":"import pandas as pd\n\nmessy = pd.read_csv(\"orders_messy.csv\")\n\nmessy[\"customer\"] = messy[\"customer\"].str.strip()\nmessy[\"country\"] = messy[\"country\"].str.strip().str.title()\nmessy[\"channel\"] = messy[\"channel\"].str.replace(\" \", \"_\").fillna(\"unknown\")\nmessy[\"first_name\"] = messy[\"customer\"].str.split(\" \").str[0]\n\ndesk_lines = messy[\"product\"].str.contains(\"desk\", case=False).sum()\nprint(messy[\"country\"].value_counts())\n","checks":[{"id":"customer","test":"(messy['customer'] == messy['customer'].str.strip()).all() and messy['customer'].nunique() == 122"},{"id":"country","test":"set(messy['country']) == {'Egypt', 'United Arab Emirates', 'Saudi Arabia', 'Jordan', 'United Kingdom', 'Germany', 'United States', 'Canada'}"},{"id":"channel","test":"set(messy['channel']) == {'web', 'mobile_app', 'marketplace', 'unknown'} and (messy['channel'] == 'unknown').sum() == 7"},{"id":"first-name","test":"'first_name' in messy.columns and messy['first_name'].iloc[0] == 'Daniel' and not messy['first_name'].str.contains(' ').any()"},{"id":"desk","test":"int(desk_lines) == 15"}],"title":"Clean the export's text columns","prompt":"<p>In <code dir=\"ltr\">messy</code>: strip spaces from <code dir=\"ltr\">customer</code>; strip and title-case <code dir=\"ltr\">country</code>; in <code dir=\"ltr\">channel</code>, replace the space with an underscore (<code dir=\"ltr\">mobile app</code> → <code dir=\"ltr\">mobile_app</code>) and fill missing values with <code dir=\"ltr\">&quot;unknown&quot;</code>. Add a <code dir=\"ltr\">first_name</code> column holding the first word of the cleaned customer name. Finally set <code dir=\"ltr\">desk_lines</code> to the number of rows whose <code dir=\"ltr\">product</code> contains “desk” in any capitalisation.</p>\n","hints":["Chain <code dir=\"ltr\">.str</code> methods and assign back to the same column: <code dir=\"ltr\">messy[&quot;country&quot;] = messy[&quot;country&quot;].str.strip().str.title()</code>.","<code dir=\"ltr\">.str.replace(&quot; &quot;, &quot;_&quot;)</code> leaves missing values missing, so you can chain <code dir=\"ltr\">.fillna(&quot;unknown&quot;)</code> right after it.","<code dir=\"ltr\">messy[&quot;customer&quot;].str.split(&quot; &quot;).str[0]</code> takes the first word. For the count, <code dir=\"ltr\">str.contains(&quot;desk&quot;, case=False).sum()</code>; and use <code dir=\"ltr\">messy[&quot;product&quot;]</code>, not <code dir=\"ltr\">messy.product</code>."],"explanation":"<p>After stripping and title-casing, the 22 country spellings collapse to exactly the eight countries Cartwheel sells to, and 122 distinct customer names remain once the padded duplicates match their trimmed twins. The channel now uses the same codes as the clean <code dir=\"ltr\">orders</code> table, so the two can be compared or joined later. <code dir=\"ltr\">str.split(&quot; &quot;).str[0]</code> takes the first piece of each split; <code dir=\"ltr\">expand=True</code> would give you all the pieces as columns. <code dir=\"ltr\">case=False</code> finds <code dir=\"ltr\">Standing Desk 140cm</code>, <code dir=\"ltr\">Compact Writing Desk</code> and <code dir=\"ltr\">Desk Mat XL</code> alike: 15 lines.</p>\n","checkLabels":{"customer":"Customer names have no leading or trailing spaces","country":"<code dir=\"ltr\">country</code> holds exactly the eight real countries","channel":"<code dir=\"ltr\">channel</code> uses <code dir=\"ltr\">web</code>, <code dir=\"ltr\">mobile_app</code>, <code dir=\"ltr\">marketplace</code> and <code dir=\"ltr\">unknown</code>","first-name":"<code dir=\"ltr\">first_name</code> holds one word per row, starting with Daniel","desk":"<code dir=\"ltr\">desk_lines</code> is 15"}},"words":694,"readMinutes":3}