Nothing arrives ready. The records were produced by a system built for a purpose that was not analysis, and they reach you carrying every historical decision anyone ever made about them: the migration that renamed half the product catalogue, the free-text field a support team repurposed as a status flag, the test accounts nobody removed. Wrangling is the process of discovering all of that and deciding how to respond, and the deciding is the half that gets underestimated. Replacing a blank with a zero, dropping rows that are missing a value, folding two spellings into one category: each is a judgement that moves the eventual answer, made quickly, early and usually without being written down anywhere. The stage deserves more respect than it gets, because most of the error in a final number is either introduced here or prevented here, usually by somebody in a hurry.
- Row counts either side of every join. A join that returns more rows than it started with has duplicated something; one that returns fewer has silently dropped something. Both are routine and neither raises an error.
- What a blank means. Not measured, measured as zero, declined and not applicable are four situations arriving as the same empty cell. Filling them all with zero is a decision about all four at once.
- Duplicates that are not duplicates. One person with two email addresses, one order written twice by a retried webhook, and two different customers sharing a name are three problems wanting three fixes.
- Dates and time zones. A timestamp with no zone attached is an open question, and daylight saving converts it into a wrong answer twice a year.
- Categories that drift. DE, Germany, germany and a version with a trailing space are one country until you group by the column.
- Units, currencies and definitions. A revenue column can be gross or net, booked or recognised, converted at the rate on the day or at today's rate, and nothing in its name will tell you which.
Do it in code, not by hand
The practical rule: the raw extract is read-only and everything done to it is a script. Open a file in a spreadsheet, sort a column, save, and the provenance has gone: nobody can establish afterwards which cells changed or why, including you. Written as code, the sequence survives, which lets a reviewer disagree with one step instead of distrusting the whole table, and turns next quarter's version into a few minutes instead of a second excavation. On tools, pandas is the sensible default, because it is what the notebooks already sitting in most teams are written in. Polars is the better engine once a file is large enough to make a laptop labour, and its expression syntax makes it harder to lose track of what a long chain of operations does. Whatever can be done in SQL should be: filtering and joining millions of rows is what a warehouse was built for, and hauling them into memory to do the same thing more slowly is the signature habit of somebody who learned pandas first. At some point this stops being wrangling at all: once the same repairs run every week on data arriving to a schedule, the work has become data engineering and belongs in dbt, where it can be tested and versioned.
