<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tobiloba Oluwayemi</title>
    <description>The latest articles on DEV Community by Tobiloba Oluwayemi (@tobiloba_oluwayemi_9c9f49).</description>
    <link>https://dev.to/tobiloba_oluwayemi_9c9f49</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4144478%2F4710a714-b846-4dbc-9036-dcda5e1b0355.png</url>
      <title>DEV Community: Tobiloba Oluwayemi</title>
      <link>https://dev.to/tobiloba_oluwayemi_9c9f49</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tobiloba_oluwayemi_9c9f49"/>
    <language>en</language>
    <item>
      <title>Clean a Messy Sales CSV with Pandas: A Step-by-Step Guide</title>
      <dc:creator>Tobiloba Oluwayemi</dc:creator>
      <pubDate>Sat, 26 Sep 2026 14:39:23 +0000</pubDate>
      <link>https://dev.to/tobiloba_oluwayemi_9c9f49/clean-a-messy-sales-csv-with-pandas-a-step-by-step-guide-l15</link>
      <guid>https://dev.to/tobiloba_oluwayemi_9c9f49/clean-a-messy-sales-csv-with-pandas-a-step-by-step-guide-l15</guid>
      <description>&lt;p&gt;Learn how to turn a broken sales export into a trustworthy dataset—and how to decide what to do with the parts you can't fix.&lt;/p&gt;

&lt;p&gt;Real-world CSV files rarely arrive perfectly clean. You may encounter inconsistent column names, extra spaces, mixed date formats, missing values, invalid quantities, malformed emails, and duplicate transactions.&lt;/p&gt;

&lt;p&gt;In this tutorial, you'll take a messy sales CSV and clean it systematically with pandas. You'll also learn an important principle of data cleaning: don't automatically "fix" data when you don't have enough information to know what the correct value should be.&lt;/p&gt;

&lt;p&gt;By the end, you'll have a cleaner dataset saved as clean_sales.csv, explicit data-quality flags, and a repeatable script you can reuse.&lt;/p&gt;

&lt;p&gt;Prerequisites&lt;/p&gt;

&lt;p&gt;You'll need:&lt;/p&gt;

&lt;p&gt;Python 3.x&lt;br&gt;
pandas 2.2.3 or later&lt;br&gt;
A CSV file named messy_sales.csv&lt;/p&gt;

&lt;p&gt;Install pandas if necessary:&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
pip install pandas&lt;/p&gt;

&lt;p&gt;The examples in this article were tested with pandas 2.2.3 and 3.0.2. One small difference is that newer pandas versions may display datetime precision as datetime64[us], while pandas 2.2.3 displays datetime64[ns]. The cleaning logic remains the same.&lt;/p&gt;

&lt;p&gt;Step 1: Load and Inspect the Data&lt;/p&gt;

&lt;p&gt;Start by importing pandas and reading the CSV file.&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
import pandas as pd&lt;/p&gt;

&lt;p&gt;df = pd.read_csv("messy_sales.csv")&lt;/p&gt;

&lt;p&gt;print(df.head())&lt;br&gt;
print(df.shape)&lt;br&gt;
print(df.columns.tolist())&lt;br&gt;
Why inspect first?&lt;/p&gt;

&lt;p&gt;Before changing anything, you need to understand what you're working with.&lt;/p&gt;

&lt;p&gt;The original file contains 30 rows and 9 columns. The column names also contain inconsistent spacing and capitalization:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
['Order ID', ' customer name ', 'EMAIL', 'Order  Date',&lt;br&gt;
 'Product Category ', 'Unit Price ($)', 'Qty', 'country', 'Status']&lt;/p&gt;

&lt;p&gt;The data also contains issues such as:&lt;/p&gt;

&lt;p&gt;inconsistent text formatting&lt;br&gt;
mixed date formats&lt;br&gt;
missing prices and quantities&lt;br&gt;
"two" instead of 2&lt;br&gt;
a negative quantity&lt;br&gt;
malformed email data&lt;br&gt;
duplicate transactions&lt;/p&gt;

&lt;p&gt;Inspecting the raw file first gives you a baseline for the cleaning process.&lt;/p&gt;

&lt;p&gt;Step 2: Standardize Column Names and Text&lt;/p&gt;

&lt;p&gt;Start by making the column names consistent.&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
df.columns = (&lt;br&gt;
    df.columns&lt;br&gt;
      .str.strip()&lt;br&gt;
      .str.replace(r"\s+", "_", regex=True)&lt;br&gt;
      .str.lower()&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;print(df.columns.tolist())&lt;/p&gt;

&lt;p&gt;You should now have:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
['order_id', 'customer_name', 'email', 'order_date',&lt;br&gt;
 'product_category', 'unit_price_($)', 'qty', 'country', 'status']&lt;/p&gt;

&lt;p&gt;Next, clean the text fields.&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
text_columns = [&lt;br&gt;
    "customer_name",&lt;br&gt;
    "email",&lt;br&gt;
    "product_category",&lt;br&gt;
    "country",&lt;br&gt;
    "status"&lt;br&gt;
]&lt;/p&gt;

&lt;p&gt;for column in text_columns:&lt;br&gt;
    df[column] = df[column].str.strip()&lt;/p&gt;

&lt;p&gt;df["customer_name"] = df["customer_name"].str.title()&lt;/p&gt;

&lt;p&gt;for column in ["email", "product_category", "country", "status"]:&lt;br&gt;
    df[column] = df[column].str.lower()&lt;/p&gt;

&lt;p&gt;This removes unnecessary whitespace and gives similar values a consistent format.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
" Amaka Obi "  →  "Amaka Obi"&lt;br&gt;
"Delivered"    →  "delivered"&lt;br&gt;
"NIGERIA"      →  "nigeria"&lt;/p&gt;

&lt;p&gt;Standardization makes later filtering, grouping, and duplicate detection much more reliable.&lt;/p&gt;

&lt;p&gt;Step 3: Convert Dates and Prices to Numeric Types&lt;br&gt;
Clean the dates&lt;/p&gt;

&lt;p&gt;First, inspect the raw dates:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
print(df[["order_id", "order_date"]].head(12))&lt;/p&gt;

&lt;p&gt;Notice these are still raw strings. You can see different formats, including an ambiguous value such as:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
01/07/2026&lt;/p&gt;

&lt;p&gt;Now convert the column:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
df["order_date"] = pd.to_datetime(&lt;br&gt;
    df["order_date"],&lt;br&gt;
    format="mixed",&lt;br&gt;
    errors="coerce"&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;Check the result:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
print(df[["order_id", "order_date"]].head(12))&lt;/p&gt;

&lt;p&gt;Using format="mixed" allows pandas to handle different date representations within the same column.&lt;/p&gt;

&lt;p&gt;The value 01/07/2026 is interpreted as January 7, 2026 by the pandas version used for this tutorial. However, this is exactly why ambiguous dates deserve attention: another dataset may use day-first formatting.&lt;/p&gt;

&lt;p&gt;The errors="coerce" argument is also important. If pandas cannot interpret a value as a date, it converts that value to NaT rather than stopping the entire cleaning process.&lt;/p&gt;

&lt;p&gt;You should therefore check for invalid dates:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
print(df["order_date"].isna().sum())&lt;/p&gt;

&lt;p&gt;If the result is greater than zero, investigate those rows before using the dates for analysis.&lt;/p&gt;

&lt;p&gt;Clean the prices&lt;/p&gt;

&lt;p&gt;The price column contains dollar signs, commas, and missing values.&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
df["unit_price"] = (&lt;br&gt;
    df["unit_price_($)"]&lt;br&gt;
      .str.replace("$", "", regex=False)&lt;br&gt;
      .str.replace(",", "", regex=False)&lt;br&gt;
      .astype(float)&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;Now unit_price is numeric, making calculations possible.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
"$1,200.00" → 1200.0&lt;br&gt;
"$199.99"   → 199.99&lt;/p&gt;

&lt;p&gt;The CSV reader automatically interprets blank fields and the file's N/A values as missing values, so the conversion works with this dataset. If literal text such as "N/A" remains a string in another CSV, .astype(float) may fail; in that case, use pd.to_numeric(..., errors="coerce") instead.&lt;/p&gt;

&lt;p&gt;Step 4: Clean and Inspect Quantities&lt;/p&gt;

&lt;p&gt;The quantity column contains a mixture of numbers, text, missing values, and a negative value.&lt;/p&gt;

&lt;p&gt;Start by converting the obvious text value:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
df["qty"] = df["qty"].replace({"two": "2"})&lt;br&gt;
df["qty"] = pd.to_numeric(df["qty"], errors="coerce")&lt;/p&gt;

&lt;p&gt;Now inspect unusual quantities:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
print(&lt;br&gt;
    df.loc[&lt;br&gt;
        df["qty"].isna() | (df["qty"] &amp;lt; 0),&lt;br&gt;
        ["order_id", "qty", "status"]&lt;br&gt;
    ]&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;Expected output:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
 order_id  qty      status&lt;br&gt;
     1005  NaN      pending&lt;br&gt;
     1021 -1.0     cancelled&lt;br&gt;
     1027  NaN      shipped&lt;/p&gt;

&lt;p&gt;There are three cases here:&lt;/p&gt;

&lt;p&gt;1005: quantity is missing.&lt;br&gt;
1021: quantity is negative, but the order is cancelled.&lt;br&gt;
1027: quantity is missing.&lt;/p&gt;

&lt;p&gt;You should not automatically replace these values with 0. A missing quantity and a zero quantity mean different things, while the negative value may have meaning because the transaction was cancelled.&lt;/p&gt;

&lt;p&gt;Instead, keep the original information and create a flag later.&lt;/p&gt;

&lt;p&gt;Step 5: Find and Handle Duplicate Transactions&lt;/p&gt;

&lt;p&gt;Duplicate detection should happen before you start filling or modifying missing values. Otherwise, changes made during cleaning can affect your ability to recognize duplicate records.&lt;/p&gt;

&lt;p&gt;Create a list of fields that describe the transaction:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
duplicate_columns = [&lt;br&gt;
    "customer_name",&lt;br&gt;
    "order_date",&lt;br&gt;
    "product_category",&lt;br&gt;
    "unit_price",&lt;br&gt;
    "qty",&lt;br&gt;
    "country",&lt;br&gt;
    "status"&lt;br&gt;
]&lt;/p&gt;

&lt;p&gt;duplicates = df[&lt;br&gt;
    df.duplicated(&lt;br&gt;
        subset=duplicate_columns,&lt;br&gt;
        keep=False&lt;br&gt;
    )&lt;br&gt;
]&lt;/p&gt;

&lt;p&gt;print(&lt;br&gt;
    duplicates[&lt;br&gt;
        ["order_id", "customer_name", "order_date",&lt;br&gt;
         "product_category", "unit_price", "qty",&lt;br&gt;
         "country", "status"]&lt;br&gt;
    ].sort_values(&lt;br&gt;
        ["customer_name", "order_date", "order_id"]&lt;br&gt;
    )&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;The result identifies these duplicate pairs:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
 order_id customer_name order_date product_category  unit_price  qty country  status&lt;br&gt;
     1001    Amaka Obi 2026-01-05       electronics      199.99  2.0 nigeria delivered&lt;br&gt;
     1007    Amaka Obi 2026-01-05       electronics      199.99  2.0 nigeria delivered&lt;br&gt;
     1004    Chidi Eze 2026-01-10              books     1200.00  1.0 nigeria delivered&lt;br&gt;
     1020    Chidi Eze 2026-01-10              books     1200.00  1.0 nigeria delivered&lt;/p&gt;

&lt;p&gt;The email column is deliberately not part of duplicate_columns. Email differences or missing emails should not automatically prevent two otherwise identical transactions from being identified as duplicates.&lt;/p&gt;

&lt;p&gt;For the first pair, keep 1001 and remove 1007.&lt;/p&gt;

&lt;p&gt;For the second pair, keep 1020 because it contains the available customer email information, while 1004 does not.&lt;/p&gt;

&lt;p&gt;Check for possible near-duplicates&lt;/p&gt;

&lt;p&gt;You should also investigate records that look similar but may represent legitimate repeat purchases.&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
near_duplicates = df[&lt;br&gt;
    df.duplicated(&lt;br&gt;
        subset=["customer_name", "product_category"],&lt;br&gt;
        keep=False&lt;br&gt;
    )&lt;br&gt;
]&lt;/p&gt;

&lt;p&gt;print(&lt;br&gt;
    near_duplicates[&lt;br&gt;
        ["order_id", "customer_name", "order_date",&lt;br&gt;
         "product_category"]&lt;br&gt;
    ].sort_values(&lt;br&gt;
        ["customer_name", "order_date", "order_id"]&lt;br&gt;
    )&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;This produces:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
 order_id customer_name order_date  product_category&lt;br&gt;
     1001    Amaka Obi 2026-01-05       electronics&lt;br&gt;
     1007    Amaka Obi 2026-01-05       electronics&lt;br&gt;
     1004    Chidi Eze 2026-01-10              books&lt;br&gt;
     1020    Chidi Eze 2026-01-10              books&lt;br&gt;
     1003 Sarah Connor 2026-01-09    home &amp;amp; kitchen&lt;br&gt;
     1013 Sarah Connor 2026-01-20    home &amp;amp; kitchen&lt;/p&gt;

&lt;p&gt;The first two pairs are the true duplicates you already found. The third pair, 1003 and 1013, has different dates, so you should keep both because they may represent separate purchases.&lt;/p&gt;

&lt;p&gt;Now remove only the confirmed duplicate records:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
df = df[~df["order_id"].isin([1007, 1004])].copy()&lt;/p&gt;

&lt;p&gt;print(df.shape)&lt;/p&gt;

&lt;p&gt;Expected result:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
(28, 10)&lt;/p&gt;

&lt;p&gt;The dataset now contains 28 transactions.&lt;/p&gt;

&lt;p&gt;Step 6: Create Data-Quality Flags&lt;/p&gt;

&lt;p&gt;Instead of silently changing questionable values, create flags that make the problems visible.&lt;/p&gt;

&lt;p&gt;Missing customer names&lt;br&gt;
python&lt;br&gt;
df["missing_customer_name"] = df["customer_name"].isna()&lt;br&gt;
Invalid emails&lt;/p&gt;

&lt;p&gt;A simple email pattern can identify obviously malformed addresses:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
email_pattern = r"^[^@\s]+@[^@\s]+.[^@\s]+$"&lt;/p&gt;

&lt;p&gt;df["invalid_email"] = (&lt;br&gt;
    df["email"].notna()&lt;br&gt;
    &amp;amp; ~df["email"].str.match(email_pattern)&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;The malformed tom.baker@example address will be flagged.&lt;/p&gt;

&lt;p&gt;The missing email associated with order 1004 no longer needs a flag because that duplicate transaction was removed in Step 5. The retained record, 1020, contains an email.&lt;/p&gt;

&lt;p&gt;You can confirm that there are no missing emails:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
print(df["email"].isna().sum())&lt;/p&gt;

&lt;p&gt;Expected output:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
0&lt;br&gt;
Missing prices&lt;br&gt;
python&lt;br&gt;
df["missing_price"] = df["unit_price"].isna()&lt;/p&gt;

&lt;p&gt;Don't replace missing prices with 0. A missing price does not mean the product was free.&lt;/p&gt;

&lt;p&gt;Missing quantities&lt;br&gt;
python&lt;br&gt;
df["missing_qty"] = df["qty"].isna()&lt;br&gt;
Negative quantities&lt;br&gt;
python&lt;br&gt;
df["negative_qty"] = df["qty"] &amp;lt; 0&lt;/p&gt;

&lt;p&gt;This preserves the negative quantity on the cancelled order while making it easy to exclude or investigate later.&lt;/p&gt;

&lt;p&gt;Step 7: Remove Redundant Columns and Verify the Result&lt;/p&gt;

&lt;p&gt;The original unit_price_($) column has already been cleaned into unit_price, so keeping both would create unnecessary duplication.&lt;/p&gt;

&lt;p&gt;Remove the raw version:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
df = df.drop(columns="unit_price_($)")&lt;/p&gt;

&lt;p&gt;Now verify the final structure:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
print(df.shape)&lt;br&gt;
print(df.columns.tolist())&lt;br&gt;
print(df.dtypes)&lt;/p&gt;

&lt;p&gt;Expected shape:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
(28, 14)&lt;/p&gt;

&lt;p&gt;The count is:&lt;/p&gt;

&lt;p&gt;8 original columns remaining after dropping unit_price_($)&lt;br&gt;
1 cleaned unit_price column&lt;br&gt;
5 data-quality flags&lt;/p&gt;

&lt;p&gt;That gives:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
8 + 1 + 5 = 14 columns&lt;/p&gt;

&lt;p&gt;You can also inspect the final records:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
print(df.head())&lt;/p&gt;

&lt;p&gt;At this point, the dataset has:&lt;/p&gt;

&lt;p&gt;standardized column names&lt;br&gt;
cleaned text fields&lt;br&gt;
parsed dates&lt;br&gt;
numeric prices&lt;br&gt;
numeric quantities&lt;br&gt;
confirmed duplicates removed&lt;br&gt;
potential near-duplicates investigated&lt;br&gt;
missing and suspicious values explicitly flagged&lt;br&gt;
the redundant raw price column removed&lt;/p&gt;

&lt;p&gt;Finally, save the cleaned dataset:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
df.to_csv("clean_sales.csv", index=False)&lt;br&gt;
Complete Script&lt;/p&gt;

&lt;p&gt;Once you've followed the individual steps, you can combine everything into one reusable script:&lt;/p&gt;

&lt;p&gt;python&lt;br&gt;
import pandas as pd&lt;/p&gt;

&lt;h1&gt;
  
  
  1. Load data
&lt;/h1&gt;

&lt;p&gt;df = pd.read_csv("messy_sales.csv")&lt;/p&gt;

&lt;h1&gt;
  
  
  2. Standardize column names
&lt;/h1&gt;

&lt;p&gt;df.columns = (&lt;br&gt;
    df.columns&lt;br&gt;
      .str.strip()&lt;br&gt;
      .str.replace(r"\s+", "_", regex=True)&lt;br&gt;
      .str.lower()&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  3. Clean text fields
&lt;/h1&gt;

&lt;p&gt;text_columns = [&lt;br&gt;
    "customer_name",&lt;br&gt;
    "email",&lt;br&gt;
    "product_category",&lt;br&gt;
    "country",&lt;br&gt;
    "status"&lt;br&gt;
]&lt;/p&gt;

&lt;p&gt;for column in text_columns:&lt;br&gt;
    df[column] = df[column].str.strip()&lt;/p&gt;

&lt;p&gt;df["customer_name"] = df["customer_name"].str.title()&lt;/p&gt;

&lt;p&gt;for column in ["email", "product_category", "country", "status"]:&lt;br&gt;
    df[column] = df[column].str.lower()&lt;/p&gt;

&lt;h1&gt;
  
  
  4. Parse dates
&lt;/h1&gt;

&lt;p&gt;df["order_date"] = pd.to_datetime(&lt;br&gt;
    df["order_date"],&lt;br&gt;
    format="mixed",&lt;br&gt;
    errors="coerce"&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  5. Clean prices
&lt;/h1&gt;

&lt;p&gt;df["unit_price"] = (&lt;br&gt;
    df["unit_price_($)"]&lt;br&gt;
      .str.replace("$", "", regex=False)&lt;br&gt;
      .str.replace(",", "", regex=False)&lt;br&gt;
      .astype(float)&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  6. Clean quantities
&lt;/h1&gt;

&lt;p&gt;df["qty"] = df["qty"].replace({"two": "2"})&lt;br&gt;
df["qty"] = pd.to_numeric(df["qty"], errors="coerce")&lt;/p&gt;

&lt;h1&gt;
  
  
  7. Find confirmed duplicates
&lt;/h1&gt;

&lt;p&gt;duplicate_columns = [&lt;br&gt;
    "customer_name",&lt;br&gt;
    "order_date",&lt;br&gt;
    "product_category",&lt;br&gt;
    "unit_price",&lt;br&gt;
    "qty",&lt;br&gt;
    "country",&lt;br&gt;
    "status"&lt;br&gt;
]&lt;/p&gt;

&lt;p&gt;duplicates = df[&lt;br&gt;
    df.duplicated(&lt;br&gt;
        subset=duplicate_columns,&lt;br&gt;
        keep=False&lt;br&gt;
    )&lt;br&gt;
]&lt;/p&gt;

&lt;p&gt;print("Confirmed duplicates:")&lt;br&gt;
print(&lt;br&gt;
    duplicates[&lt;br&gt;
        ["order_id", "customer_name", "order_date",&lt;br&gt;
         "product_category", "unit_price", "qty",&lt;br&gt;
         "country", "status"]&lt;br&gt;
    ].sort_values(&lt;br&gt;
        ["customer_name", "order_date", "order_id"]&lt;br&gt;
    )&lt;br&gt;
)&lt;/p&gt;

&lt;h1&gt;
  
  
  8. Remove confirmed duplicates
&lt;/h1&gt;

&lt;p&gt;df = df[~df["order_id"].isin([1007, 1004])].copy()&lt;/p&gt;

&lt;h1&gt;
  
  
  9. Create data-quality flags
&lt;/h1&gt;

&lt;p&gt;df["missing_customer_name"] = df["customer_name"].isna()&lt;/p&gt;

&lt;p&gt;email_pattern = r"^[^@\s]+@[^@\s]+.[^@\s]+$"&lt;/p&gt;

&lt;p&gt;df["invalid_email"] = (&lt;br&gt;
    df["email"].notna()&lt;br&gt;
    &amp;amp; ~df["email"].str.match(email_pattern)&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;df["missing_price"] = df["unit_price"].isna()&lt;br&gt;
df["missing_qty"] = df["qty"].isna()&lt;br&gt;
df["negative_qty"] = df["qty"] &amp;lt; 0&lt;/p&gt;

&lt;h1&gt;
  
  
  10. Remove redundant raw price column
&lt;/h1&gt;

&lt;p&gt;df = df.drop(columns="unit_price_($)")&lt;/p&gt;

&lt;h1&gt;
  
  
  11. Verify and export
&lt;/h1&gt;

&lt;p&gt;print("Final shape:", df.shape)&lt;br&gt;
print(df.columns.tolist())&lt;/p&gt;

&lt;p&gt;df.to_csv("clean_sales.csv", index=False)&lt;br&gt;
Final Takeaway&lt;/p&gt;

&lt;p&gt;Good data cleaning is not about forcing every value into a convenient format. It is about making the data consistent, usable, and transparent.&lt;/p&gt;

&lt;p&gt;For this dataset, you:&lt;/p&gt;

&lt;p&gt;standardized names and text&lt;br&gt;
converted dates and numeric fields&lt;br&gt;
handled obvious formatting errors&lt;br&gt;
identified suspicious quantities&lt;br&gt;
detected and removed confirmed duplicates&lt;br&gt;
investigated near-duplicates instead of deleting them blindly&lt;br&gt;
created flags for unresolved data-quality problems&lt;br&gt;
verified the final structure before exporting&lt;/p&gt;

&lt;p&gt;Most importantly, the process preserves information you cannot confidently repair. A missing price remains identifiable as missing, and a negative quantity remains visible rather than being silently changed.&lt;/p&gt;

&lt;p&gt;Next Steps&lt;/p&gt;

&lt;p&gt;Once the dataset is clean, you can build on it by:&lt;/p&gt;

&lt;p&gt;calculating revenue by product category while excluding incomplete records with the quality flags&lt;br&gt;
writing automated tests that assert your cleaning rules and expected row counts&lt;br&gt;
validating the final DataFrame with a library such as pandera&lt;/p&gt;

&lt;p&gt;The goal is not just a cleaner CSV. It is a repeatable data-cleaning process you can trust.&lt;/p&gt;

</description>
      <category>python</category>
      <category>pandas</category>
      <category>datacleaning</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
