- Learn
- Computer Science & IT
- Data Science & AI
- Data Analysis with Python
- Wrangling Data with pandas
Wrangling Data with pandas
IntermediateLesson 4 of 8 in Data Analysis with Python Course
What you will learn
- Detect missing values and duplicate rows in a pandas DataFrame, decide whether to drop, fill or keep them, and check what each choice does to the data.
- Detect missing values in a DataFrameRecognize how pandas marks missing data, count it per column and per row, and spot hidden codes that stand for missing values.
- Drop or fill missing valuesRemove incomplete rows or columns with dropna, or fill gaps with fixed values, statistics, neighboring values or interpolation, and choose the method that fits the data.
- Find and remove duplicate rowsMark and remove repeated rows with duplicated and drop_duplicates, compare only key columns when needed, and keep the right copy of each record.
- Detect missing values in a DataFrame
- Change the values in a column in a controlled way: replace codes, map values through a lookup, convert types, rename labels, and turn numbers into groups, scaled values and indicator columns.
- Replace, map and convert valuesSwap exact values with replace, translate values through a dictionary or function with map, rename rows and columns, and convert text to numbers safely.
- Bin, scale, cap and encode valuesGroup numbers into intervals with cut and qcut, rescale columns, limit extreme values with clip, and create indicator (dummy) columns for categories.
- Replace, map and convert values
- Standardize and search text columns with the str accessor and regular expressions, and store columns with a fixed set of values efficiently as categoricals.
- Clean text columns with string methodsUse the vectorized str methods to trim, change case, split, search, replace and combine text in every row of a column at once.
- Match and extract patterns with regular expressionsWrite simple regular expressions and use them with contains, extract, findall and replace to test, pull out and clean structured pieces of text.
- Use the category dtypeConvert columns with a limited set of values to categoricals, work with their categories and codes, and use ordered categories for values that have a natural order.
- Clean text columns with string methods
- Bring data from several tables together: match rows by key columns or index labels with merge and join, fill gaps from a second source, and stack tables with concat.
- Merge DataFrames on key columnsCombine two tables by matching values in key columns with merge, choose the join type, and check the result for unmatched and multiplied rows.
- Join on the index and fill gaps from another tableMerge on index labels with join or left_index and right_index, and use combine_first and label alignment to patch missing values from a second source.
- Stack DataFrames with concatGlue tables together along rows or columns with pd.concat, control the resulting index and columns, and label the source of each part.
- Merge DataFrames on key columns
- Work with tables that have several index levels, and move data between long and wide layouts with stack, unstack, pivot and melt.
- Work with hierarchical indexesBuild and read a MultiIndex, select data by outer and inner levels, reorder and sort levels, and move levels between the index and the columns.
- Reshape with stack and unstackRotate column labels into the index with stack and index levels into columns with unstack, and handle the missing combinations that reshaping exposes.
- Convert between long and wide format with pivot and meltRecognize long and wide layouts, turn long data into a wide table with pivot or pivot_table, and turn a wide table into long data with melt.
- Work with hierarchical indexes
Wrangling Data with pandas study guide
1Find and handle missing values and duplicates
A gap, a zero, and a repeated record have different meanings. Learn to measure missingness before choosing what to keep, remove, or estimate, and to identify duplicates using the columns that define a record. Small weather and order tables show how pandas makes these decisions and how they affect row counts, dtypes, and statistics. Each change has a visible result you can check before continuing with an analysis.
1.1 Find the gaps before calculating
Missing data means a value is unknown or unavailable. pandas stores a special marker in its place. Detect the marker before calculating totals or deciding which rows are usable.
Recognize the markers
The marker depends on the dtype. Float columns use NaN, datetime columns use NaT, and nullable types such as "Int64", "boolean", and "string" use pd.NA. pandas 3 infers text columns as str, with NaN for missing text. Python's None is also recognized as missing. Use import pandas as pd and import numpy as np for the examples.
df.isna() produces a Boolean table with the same rows and columns as df. A True marks a gap; df.notna() marks present values. The aliases isnull() and notnull() do the same jobs. Do not compare a column with np.nan: NaN is unequal even to itself, so s == np.nan misses the gaps.
Count cells and affected rows
Consider these weather readings:
| city | temp_c | rain_mm |
|---|---|---|
| Lima | 18.0 | 0.0 |
| Oslo | NaN | 4.0 |
| Pune | 31.0 | NaN |
| Kyiv | NaN | NaN |
A Boolean True counts as 1, so df.isna().sum() gives 0 missing cities, 2 temperatures, and 2 rainfall readings. df.isna().mean() gives shares of 0.0, 0.5, and 0.5. Each column's share uses all four rows as its denominator.
To find affected rows, use df.isna().any(axis=1). It returns False, True, True, True; summing gives 3. In contrast, df.isna().sum().sum() gives 4 missing cells. Kyiv contributes two cells but only one affected row. Use the measure that answers your question.
Recognize hidden missing codes
An instrument may record an unavailable reading as -999, "-", or "unknown". These are ordinary values until you convert them. After confirming the code's meaning, use df.replace([-999, "unknown"], np.nan). During CSV loading, na_values=[-999, "unknown"] adds these codes to the recognized markers. Empty CSV fields and common strings such as "NA", "n/a", and "NULL" are already recognized by default.
A zero rainfall reading can mean no rain, so replacing every zero would destroy real observations. Also, an empty string created in memory, "", and a string containing spaces are not missing to isna(). Inspect stored text as well as marker counts. To include missing categories in a frequency table, use value_counts(dropna=False).
Check calculations and dtypes
For s = pd.Series([4, np.nan, 8]), s.mean() is 6.0: it uses the two known values. s.count() is 2, while len(s) is 3. Dividing s.sum() by len(s) gives 4.0 and wrongly treats the gap as zero. Most statistics skip gaps by default; an all-missing column has sum 0 but mean NaN.
A gap can change storage too. pd.Series([3, None, 5]) becomes float64, holding 3.0, NaN, and 5.0. Giving dtype="Int64" keeps integer values with <NA> for the gap. The capital I identifies pandas' nullable integer dtype.
Key Takeaways
- Use
isna()to detect missing markers across dtypes. - Count missing cells separately from rows with at least one gap.
- Convert confirmed missing codes; preserve valid zeros.
- Check the number of known values used in a statistic.
- Use
"Int64"when integer values need a missing marker.
1.2 Choose what to drop or fill
Dropping a row removes its known values along with its gaps. Filling a gap keeps the row but introduces an estimate. Choose based on what the observation means and which columns the analysis needs.
Control which rows are removed
df.dropna() removes every row with any missing value. On the weather table, only Lima remains. Narrow the decision with subset, or require a minimum number of present cells with thresh.
| Call | Rows kept | Reason |
|---|---|---|
df.dropna(subset=["temp_c"]) | Lima, Pune | Temperature is present |
df.dropna(thresh=2) | Lima, Oslo, Pune | At least two cells are present |
df.dropna(how="all") | All four | Every row has a city |
axis="columns" drops columns with gaps and keeps only city here. On a wide table, the default row rule may discard much of the data because a gap in any column is enough. Compare len(df) before and after removal.
Assign the result
Learn the reassignment form: df = df.dropna(). By default, cleaning methods return a new object. pandas 3.1 deprecates inplace=True for methods including dropna, drop_duplicates, and rename.
For one column, write df["temp_c"] = df["temp_c"].fillna(0). A chained call such as df["temp_c"].fillna(0, inplace=True) never updates the parent DataFrame under pandas 3 Copy-on-Write. Assignment makes the destination explicit.
Fill with a compatible value
A per-column dictionary lets a mixed table receive suitable values: df.fillna({"rain_mm": 0, "city": "Unknown"}). Columns omitted from the dictionary keep their gaps. A scalar such as 0 is convenient for an all-numeric table whose columns all accept it. Filling text with a number is incompatible; pandas 3.1 warns about this and a future version will reject it.
The known temperatures are 18.0 and 31.0. Their mean is 24.5, so df["temp_c"].fillna(df["temp_c"].mean()) puts 24.5 into both gaps. An extreme value can pull the mean strongly; a median is less sensitive. Before filling, keep an indicator with df["temp_missing"] = df["temp_c"].isna() so estimated and observed values remain distinguishable.
Mean filling reduces the apparent spread of the column. A distant fixed value can increase it. Filling rainfall with 0 asserts that no rain fell, which is inappropriate for a sensor that was off.
Use neighboring readings when order matters
ffill() carries the last known value forward; bfill() carries the next known value backward. For 12.0, NaN, NaN, NaN, 15.0, forward filling gives four 12.0 values followed by 15.0. ffill(limit=1) fills only the first gap in that run. A leading gap has no previous value and stays missing. Use ffill() directly; fillna(method="ffill") was removed in pandas 3.
interpolate() estimates between neighboring positions. For 10, NaN, NaN, 16, linear interpolation gives 10, 12, 14, 16. It fits smoothly changing measurements in a meaningful order. Carrying or interpolating across unrelated cities would impose a relationship the rows do not have.
After any operation, rerun isna().sum() as well as checking row counts. A successful method call tells you that the code ran; these checks tell you what changed.
Key Takeaways
- Use
subset,how, orthreshto express which gaps matter. - Assign returned results to the DataFrame or column you intend to change.
- Choose fill values that fit both the column dtype and the meaning of the gap.
- Preserve a missingness indicator before filling estimated values.
- Use neighboring-value methods only when row order is meaningful.
- Check row counts and missing counts after cleaning.
1.3 Identify the record before removing its copies
Two rows can look alike without describing the same event. Before removing duplicates, decide which values identify one record. An order ID may identify an order; customer and date alone may identify several legitimate purchases.
Mark the extra copies
df.duplicated() returns one Boolean per row. By default it compares every column, keeps the first occurrence, and marks later copies True. Summing the result counts extra copies, not all members of repeated groups.
Consider six order rows with IDs 101, 102, 102, 103, 104, 104. The two 102 rows are identical. The two 104 rows have different recorded weights, 0.5 kg and 0.8 kg. With every column compared, there is one extra copy: df.duplicated().sum() is 1. df.drop_duplicates() keeps five rows because the two different 104 rows are not exact copies.
Compare the columns that define identity
df.duplicated(subset=["order_id"]) compares IDs alone. It marks the second 102 and second 104, giving two extra copies. The other column values no longer affect this decision. Inspect the conflicting weights before choosing which order record to keep.
keep value | Repeated group behavior |
|---|---|
"first" | Keep the first row in the current order |
"last" | Keep the last row in the current order |
False | Remove every member of a repeated group |
df.drop_duplicates(subset="order_id", keep="last") keeps the 0.8 kg version of order 104 in this order. Keeping the last row does not itself mean keeping the newest record. Establish that order first:
latest = df.sort_values("updated").drop_duplicates(
subset="customer_id", keep="last"
)
Sorting by updated puts older records before newer ones. The duplicate check then compares only customer_id, so differing update times do not prevent two rows from being recognized as versions of the same customer.
Separate row values from index labels
These methods compare values, ignoring index labels. Identical rows labeled 4 and 9 still count as duplicates. Missing values at corresponding positions count as equal for this check. To test repeated labels themselves, use df.index.duplicated().
Exact comparison also preserves differences in case and whitespace. The values " Lisbon", "lisbon", and "LISBON " are distinct. Clean with .str.strip().str.lower() before checking duplicates when those spellings represent the same city. Otherwise nunique() will count three names for one place.
Check uniqueness where the relationship needs it
A lookup table should have one row per lookup key when each record must receive one set of details. lookup["customer_id"].is_unique checks that requirement. Repeated customer IDs in an orders table are normal because a customer can place several orders.
If a lookup key repeats, a later merge repeats each matching order for every matching lookup row. Removing duplicates from the orders would discard genuine purchases without fixing the lookup. Check the intended key on the correct table, inspect conflicting records, and compare the row count after removal.
Key Takeaways
- Define a record with the correct key columns before removing copies.
duplicated().sum()counts later copies with the defaultkeep="first".- Sort by update time before keeping the latest record.
- Row-value duplicates and repeated index labels are separate checks.
- Lookup-key uniqueness prevents accidental row multiplication in merges.
2Transform values by replacing, mapping and binning
Transforming a column changes what its values mean or how they are represented. Choose between exact replacements, lookups, numeric conversion, intervals, scaled values, and indicator columns. You will work through the inputs and outputs of each method, including what happens to unmatched values and boundary values, so a convenient operation does not silently change categories or lose usable measurements.
2.1 Change values and labels deliberately
A replacement edits matching values, a lookup translates values, and a rename changes labels. These jobs can look similar in code but produce different results when a value is absent from a dictionary.
Replace exact values
s.replace("N/A", np.nan) changes only cells equal to "N/A". A list can name several old values: s.replace(["-", "?"], np.nan). A dictionary provides separate replacements, such as s.replace({"M": "male", "F": "female"}). Unmatched values remain as they were.
For a whole table, a nested dictionary restricts the edit to one column: df.replace({"status": {0: "open", 1: "closed"}}). This avoids changing unrelated zeros and ones elsewhere. Assign the returned result to keep the change.
replace matches entire cells. With "Main St" and "St", s.replace("St", "Street") changes only "St". To edit parts of a string, use s.str.replace("St", "Street", regex=False), which also produces "Main Street".
Translate with a lookup
For codes "PT", "JP", and "KE", compare the same dictionary in two methods:
| Method | PT | JP | KE |
|---|---|---|---|
s.map({"PT": "Portugal", "JP": "Japan"}) | Portugal | Japan | NaN |
s.replace({"PT": "Portugal", "JP": "Japan"}) | Portugal | Japan | KE |
A dictionary passed to map defines the available outputs. A missing key has no output and becomes missing. Check isna().sum() afterward so an incomplete lookup is visible.
map also accepts a function: s.map(lambda x: x * 1000) converts kilometer values to meters. For this arithmetic, s * 1000 is simpler and faster because it operates on the whole column. DataFrame.map applies a function cell by cell; it replaces the removed applymap method.
Convert text to numbers
astype requests a dtype, such as "float64", "Int64", or "category". It raises an error if a value cannot be converted. For text "12", "7.5", and "n/a", pd.to_numeric(s, errors="coerce") instead produces 12.0, 7.5, and NaN. Coercion preserves readable numbers but requires inspection of the failures.
Formatting can block conversion even when the number is valid. For "1,200" and "950", first remove the thousands separator:
cleaned = s.str.replace(",", "", regex=False)
numbers = pd.to_numeric(cleaned, errors="coerce")
failed = s.notna() & numbers.isna()
Here failed marks values that were present before conversion but became missing. Comparing missing counts alone gives a useful total; the mask also locates the original text to inspect. Remove known units from measured text in the same deliberate way before converting.
Rename the labels
df.rename(columns={"Qty": "quantity"}) changes a column heading, leaving its cells unchanged. df.rename(columns=str.lower) lowercases all headings. A sequence such as df.columns.str.strip().str.lower().str.replace(" ", "_", regex=False) turns "Total KG " into total_kg; assign it to df.columns.
Use replace or map for values inside a column. Use rename for the labels you select those values by.
Key Takeaways
replacepreserves unmatched values; dictionarymapmakes them missing.- Use
.str.replaceto edit part of each text value. - Prefer vectorized arithmetic for simple column calculations.
- Inspect newly missing values after numeric coercion.
renamechanges labels and leaves data values unchanged.
2.2 Group and rescale measurements
Some analyses need exact measurements, while others need groups or comparable numeric ranges. Keep the original meaning clear: binning creates categories, scaling changes the numeric scale, and capping replaces extreme values.
Put values into intervals
pd.cut(ages, [18, 30, 45, 65]) uses the intervals (18, 30], (30, 45], and (45, 65]. The parenthesis excludes the left endpoint; the square closing symbol includes the right endpoint. Ages 19, 25, and 30 enter the first interval, 41 the second, and 64 the third. Age 18 becomes missing because it is on the excluded left edge.
include_lowest=True includes that lowest value in the first interval. right=False instead makes left-closed intervals such as [18, 30), so 30 enters the second interval. Values outside all intervals remain missing. Test the exact edges, as well as typical values between them.
There must be one label per interval, one fewer than the number of edges. With labels=["18-30", "31-45", "46-65"] and include_lowest=True, the six ages above give counts 4, 1, and 1. These labels describe whole-number ages; the interval boundaries still govern assignment.
Choose boundaries or group sizes
| Operation | What it controls | Useful purpose |
|---|---|---|
pd.cut(x, [18, 30, 45, 65]) | Fixed boundaries | Meaningful ranges |
pd.cut(x, 4) | Four equal-width intervals | Compare spans of values |
pd.qcut(x, 4) | Four groups with about equal counts | Compare ranks in the data |
For 2, 4, 6, 8, 10, 12, 14, 16, qcut gives two values per quartile group. Equal-count groups need not cover equal numeric widths.
Scale without creating groups
Min-max scaling uses (x - x.min()) / (x.max() - x.min()). For 10, 20, 40, the range is 30: the results are 0, about 0.333, and 1. Both the minimum and maximum are needed; dividing by the maximum alone is a different operation. A constant column has a zero range, so this expression cannot give it a useful scaled spread.
Standardizing uses (x - x.mean()) / x.std(). For 10, 20, 30, the mean is 20 and pandas' default sample standard deviation is 10. Subtracting 20 and dividing by 10 gives z-scores -1, 0, and 1. A z-score describes distance from the mean in standard-deviation units, not an interval label.
Cap and encode
pd.Series([5, 12, 250, 8]).clip(upper=100) gives 5, 12, 100, 8. Unlike a filter, clip keeps the row and replaces the extreme number. Inspect large values with describe() or a filter before capping: a genuine large measurement is still data.
pd.get_dummies(s) creates one Boolean indicator column per category. With red, green, red, blue, each row is True only in its color's column. Use dtype=int for zeros and ones. Missing values give all-false indicators unless dummy_na=True adds a missing-value column. For ordinary least-squares regression with an intercept, drop_first=True removes a redundant category column; other models may retain every category. With one category per row, the full set of indicators sums to the intercept's constant 1, creating linear dependence.
For cells containing several tags, s.str.get_dummies(sep="|") splits a value such as "Action|Comedy" into separate indicators. Binned intervals can also be passed to get_dummies, giving a column for each range.
Key Takeaways
- Check interval endpoints and out-of-range values with
cut. cutcontrols ranges;qcutaims for similar group counts.- Min-max scaling and z-scores preserve numeric values on a changed scale.
clipcaps values without removing rows.- Indicator columns represent category membership without inventing a numeric order.
- Handle missing categories explicitly when their indicators matter.
3Clean text and categorical data
Text values need consistent spelling and spacing before they can work as categories or keys. Apply string methods across a column, extract structured pieces with regular expressions, and choose a categorical dtype when a small set of labels repeats. The examples connect text cleaning to duplicate checks and merges, while showing how stored codes and category order differ from the words learners see in a table.
3.1 Make text values consistent
A person sees the same city in " Lisbon", "lisbon", and "LISBON ". pandas sees three different strings. Cleaning a text column gives later counts, duplicate checks, and merges a consistent set of values to compare.
Apply string methods across a Series
Python's "Lima ".strip() works on one string. For a column, use the .str accessor: s.str.strip() applies trimming to every non-missing value. s.lower() raises an AttributeError because the Series itself has no lower method. Use s.str.lower().
pandas 3 normally infers text as dtype str, replacing the older object default. The str dtype holds text and missing values, with NaN for missing text. String methods work on both types. Assign text to a str column; convert a number to text first if it belongs there as text.
Combine compatible cleaning operations and assign the result:
df["city"] = df["city"].str.strip().str.lower()
The three Lisbon spellings become "lisbon". strip() removes outer whitespace, not spaces inside a name. upper() makes all letters uppercase, while title() capitalizes each word, turning "new york" into "New York". Choose one consistent form for keys and counts.
Split structured strings
For "Silva, Ana" and "Tanaka, Yui", s.str.split(", ") returns lists such as ["Silva", "Ana"]. Add expand=True to produce separate columns. Retrieve a piece from each list with .str[0] or .str.get(1).
If only the first separator should divide a value, use n=1. Splitting "a,b,c" at commas with n=1 gives "a" and "b,c". Without a limit, it gives three pieces. Match the separator exactly, including any space that is part of the format.
Search and filter
s.str.contains("lis", case=False) checks for text anywhere while ignoring case. s.str.startswith("ID") checks the beginning; s.str.endswith(".csv") checks the end. The Boolean results can select rows:
mask = df["city"].str.contains("san", case=False, na=False)
selected = df[mask]
For the pandas 3 str dtype, missing text gives False in these search tests. Writing na=False also makes the intended handling clear when using nullable string data. contains reads a regex pattern by default; add regex=False for literal text that contains special characters.
Replace parts and build new strings
s.str.replace("kg", "", regex=False) removes "kg" wherever it appears. s.replace("kg", "") changes only cells that are exactly "kg". This distinction matters for values such as "12 kg".
str.replace treats its search text literally by default. For "3.5 kg", .str.replace(".", "", regex=False) removes the period and gives "35 kg". With regex=True, the period is a pattern for almost any character and removes all characters in this single-line example. Never use that change to parse a decimal measurement.
s.str.len() counts characters; a missing value produces NaN and can make the result a float column. s.str[:3] extracts the first three characters. To combine columns, df["first"] + " " + df["last"] joins Ana and Silva as "Ana Silva"; str.cat is another joining method. Each operation returns a result, so save it in the column you intend to use.
Key Takeaways
- Use
.strto apply text methods to a Series. - Trim and standardize case before matching text keys.
- Control split output with
expandandn. - Choose literal or regex searches explicitly for special characters.
- Assign cleaned text back to retain the change.
3.2 Extract the pieces a text pattern describes
A regular expression, or regex, describes a pattern in text. It can recognize a quantity even when the number of digits or the spacing varies. Start with a small pattern whose parts you can explain.
Read the pattern pieces
| Pattern | Meaning |
|---|---|
\d | One digit |
\w | A word character: a letter, digit, or underscore |
\s | One whitespace character |
[A-Z] | One uppercase letter in this range |
+ | One or more of the preceding item |
* | Zero or more of the preceding item |
? | The preceding item is optional |
{4} | Exactly four repetitions |
(…) | A capture group |
Write Python patterns as raw strings, such as r"\d+", so Python preserves their backslashes. A period matches any character except a newline by default. Escape it as r"\." to find an actual period. For a plain-text search, regex=False avoids pattern interpretation entirely.
Choose how much text must match
contains finds a pattern anywhere, match starts at the beginning, and fullmatch requires the entire value to fit. For "123a", s.str.contains(r"\d+") and s.str.match(r"\d+") succeed, but s.str.fullmatch(r"\d+") fails. Use the whole-string test when checking a complete code. case=False or flags=re.IGNORECASE after import re allows case-insensitive matching where supported.
Capture a measurement
Take "Rice 5 kg", "Flour 2kg", "Salt", and "Beans 12 kg, 3 kg". The pattern r"(\d+)\s*kg" means one or more digits captured as a group, followed by zero or more spaces and then "kg".
quantity = s.str.extract(r"(\d+)\s*kg", expand=False)
quantity = pd.to_numeric(quantity)
The extracted strings are "5", "2", a missing value, and "12". Conversion gives numeric values 5.0, 2.0, NaN, and 12.0. extract takes the first match in each row, so the second quantity in the beans description is not included. Without expand=False, a single group returns a one-column DataFrame.
Named groups label the output. s.str.extract(r"(?P<year>\d{4})-(?P<month>\d{2})") finds "2024-03" inside "Report 2024-03 final" and returns text columns year and month. The pattern recognizes the digit layout; numeric conversion is a separate step.
Find every match or clean by pattern
findall(r"\d+") gives a list per row, including ["12", "3"] for the beans text and [] for "Salt". count(r"\d+") counts matches. extractall gives one row per match with an extra index level named match.
With regex=True, s.str.replace(r"\s+", " ", regex=True) reduces runs of whitespace to one space. r"\D" means a non-digit, so removing it turns "+351 912-345-678" into "351912345678". The uppercase forms \S and \W similarly negate whitespace and word-character classes.
Repetition is greedy by default. In "<b>x</b>", r"<.*>" reaches from the first opening symbol to the last closing symbol. Removing that match removes everything. The lazy pattern r"<.*?>" matches each tag separately and leaves "x". Check sample outputs before using a pattern across a column.
Key Takeaways
- Use raw strings to preserve regex backslashes.
- Choose
contains,match, orfullmatchfor the intended scope. - Capture groups return text; convert extracted numbers separately.
extracttakes the first match;findallandextractallretain multiple matches.- Escape special characters for literal regex matches.
- Check greedy patterns against examples with several possible matches.
3.3 Store repeated labels as categories
A column with a few values repeated many times can use the category dtype. It stores the distinct labels once and represents each row with a small integer code. The visible value is still the label.
Understand categories and codes
For sizes M, S, L, M, M, s.astype("category") infers the categories L, M, S in alphabetical order. The codes are 1, 2, 0, 1, 1. Use s.cat.categories to inspect the labels and s.cat.codes to inspect the identifiers. A code of 0 means the first stored category; it is not a measured quantity.
Repeated labels can save considerable memory. With up to 126 categories, codes use one byte each; at 127 categories they use two bytes. In a pandas 3.1 setup with PyArrow, three country names repeated across 3,000,000 rows used about 3 MB as categorical data, about 42 MB as str, and about 165 MB as object. The text storage backend affects these figures. A column with almost all unique values gets no benefit from storing a category list plus a code per row.
Define meaningful order
Alphabetical size order puts L before M before S. Define the actual size order explicitly:
size_type = pd.CategoricalDtype(["S", "M", "L"], ordered=True)
sizes = s.astype(size_type)
Now sorting follows S, M, L, comparisons such as sizes > "S" are meaningful, and sizes.max() is "L" when L occurs. For unordered labels such as countries, equality tests work but greater-than comparisons raise a TypeError. as_ordered() uses the existing category sequence; inspect it before declaring that sequence meaningful.
Change the allowed labels
You cannot assign "XL" to a categorical that allows only S, M, and L. First assign the expanded result, sizes = sizes.cat.add_categories("XL"), then assign the new value.
Converting text to a fixed category list can turn unlisted values into missing values. pandas 3.1 warns that this lossy conversion will become an error. Check the source values with value_counts() before conversion, especially after cleaning spellings.
The .cat methods serve different purposes:
rename_categories({"S": "small"})changes a label while keeping its members.remove_categories("L")removes an allowed label and makes existing L values missing.set_categories([...])replaces the whole category list.reorder_categories([...], ordered=True)changes the order without changing which labels are allowed.remove_unused_categories()removes labels with no current members.
value_counts() displays every allowed category, including those with zero observations. With an S/M/L type containing only S and M, L still appears with count 0. This exposes an empty group that ordinary text data has no stored label for.
Keep categories useful downstream
cut produces categorical intervals, and get_dummies converts category membership into indicator columns. Giving unordered country codes to a model as numbers would impose an artificial order and distance. Indicators avoid that interpretation.
Concatenating categorical columns with different category lists normally loses the categorical dtype. Align their category lists first, or use pd.concat(..., union_categories=True) in pandas 3.1 when combining the lists is intended. Inspect both the resulting dtype and the category order.
Key Takeaways
- Categoricals store a label list plus integer identifiers.
- Declare category order explicitly when ranking has meaning.
- Add a category before assigning a new label.
- Removing a used category makes its values missing.
- Use indicators for unordered categories in numeric models.
- Align category lists or deliberately union them before concatenation.
4Combine DataFrames
Combining tables can add information, add observations, or fill gaps from a backup. Those are different tasks. Match explicit keys with merge, work with index labels through join and combine_first, and stack compatible pieces with concat. Before trusting a result, predict which rows should survive and how many matches each key should produce; the examples show why unexpected growth often starts with a repeated lookup key.
4.1 Predict what a merge will keep
A merge matches rows through key values and places their information side by side. The key must describe the same thing in both tables, such as the customer ID on an order and the customer ID in a lookup table.
Name the key and choose the join
Use orders.merge(customers, on="customer_id"). Without on, pandas uses all column names shared by both tables. An unrelated shared column such as date could then prevent valid customer matches.
Suppose orders have customer IDs 10, 20, 20, 30, 50. The customer table has one row each for 10, 20, 30, 40.
how | What survives | Result rows |
|---|---|---|
"inner" | Matching keys on both sides | 4 |
"left" | Every left row and its matches | 5 |
"right" | Every right row and its matches | 5 |
"outer" | Keys from either side | 6 |
"cross" | Every left/right row pair | 20 |
Inner is the default. For a left merge, customer 50 has missing customer details. A right merge includes customer 40 without an order, plus both orders for 20. An outer merge includes both unmatched keys. A cross merge needs no key because it deliberately pairs every row. pandas 3 also provides left_anti and right_anti for rows with no matching key on the other side.
Check how many matches a row can have
Two left rows for key 20 and three right rows for key 20 produce six pairs. The merge does not choose one right row. In the example, adding a second customer lookup row for 20 increases the inner result from four rows to six.
Express the intended relationship with validate:
merged = orders.merge(
customers, on="customer_id", how="left",
validate="many_to_one"
)
This allows many orders per customer but requires unique right-hand customer IDs. A repeated right key raises MergeError. one_to_one requires uniqueness on both sides; one_to_many requires it on the left. many_to_many allows repeats on both sides and does not check uniqueness.
Investigate unmatched rows
An outer merge with indicator=True adds _merge. Here its counts are four both, one left_only, and one right_only. These are rows, not distinct keys: repeated orders can produce several both rows for one customer.
To count distinct unmatched order-side keys, use m.loc[m["_merge"] == "left_only", "customer_id"].nunique(). Inspecting the rows can reveal missing lookup entries or inconsistent text.
Control names and key formats
If keys have different names, use left_on="customer_id", right_on="id". Both key columns remain available. Several keys can be matched together with on=["city", "year"]; both must agree.
Overlapping non-key names receive _x and _y. Choose meaningful suffixes such as suffixes=("_order", "_ship") to keep the origins clear.
Keys need compatible dtypes and equal values. Numeric 1 can match numeric 1.0, but integer and text key columns can raise a merge error. Convert their formats deliberately. "ana" and "Ana " differ as text; standardize case and outer spaces before merging when they identify the same person.
Missing keys also need inspection: pandas matches missing keys on one side with missing keys on the other. If both sides contain several such rows, those matches can multiply too.
Key Takeaways
- Name merge keys explicitly.
- Choose the join type by which source rows must survive.
- Repeated keys produce every matching pair.
- Use
validateto check the intended key relationship. - Use an outer merge with an indicator to inspect unmatched rows.
- Check key values, formats, and suffixes before using the result.
4.2 Combine values at matching index labels
An index can hold the key you want to match. pandas uses those labels when joining tables, patching missing values, or adding two Series. Row position alone does not decide the matches.
Join on index labels
left.join(right) matches the two indexes and defaults to a left join. In contrast, merge defaults to an inner join. You can also write left.merge(right, left_index=True, right_index=True) when both keys are in the index.
Suppose stations has index labels A, B, C and elevations 120, 850, 15 m. A readings table has station index labels A, B, B, D. readings.join(stations) keeps four rows and adds elevations 120, 850, 850, and NaN. The two B readings both receive the B elevation. With how="inner", only the three A/B readings survive.
If the readings keep station IDs in an ordinary column, use readings.join(stations, on="station"). The left column matches the right index. The equivalent merge form is readings.merge(stations, left_on="station", right_index=True, how="left").
Keep the key location explicit
set_index("station") moves the station column into the index. reset_index() restores it as an ordinary column. These let you choose a layout that makes the match easy to inspect. left.join([t1, t2]) can join several tables sharing index labels at once.
If both joined tables contain a non-key column such as source, join raises a ValueError unless you distinguish the names. In pandas 3.1, write suffixes=("_left", "_right"); this replaces the older lsuffix and rsuffix arguments. The suffixes keep both versions without deciding which value is authoritative.
Patch gaps from a backup
a.combine_first(b) keeps known values in a, fills its gaps from b at matching labels, and includes labels found only in b.
| Label | a | b | a.combine_first(b) |
|---|---|---|---|
| a | 1 | 10 | 1 |
| b | NaN | 20 | 20 |
| c | 3 | 30 | 3 |
| d | absent | 40 | 40 |
The result keeps 1 and 3 because the first source already knows them. It uses 20 to fill the gap and adds label d with 40. Reversing the call changes which source has priority. This is useful when a main data source should override a backup, even when their row orders differ.
Recognize alignment in arithmetic
Let x have a = 5 and b = 7; let y have b = 1 and c = 2. x + y gives b = 8, with missing results at a and c because each lacks one operand. It does not add the first value of x to the first value of y.
x.add(y, fill_value=0) supplies zero for the missing side, producing a = 5, b = 8, c = 2. Use that rule when an absent contribution should count as zero. Label alignment is the same organizing idea behind these operations, but their value rules differ: join adds columns, combine_first fills gaps, and arithmetic computes with matched values.
Key Takeaways
joindefaults to a left index match;mergedefaults to inner.- In
join(on="station"), the left column matches the right index. - Use suffixes to distinguish overlapping non-key columns.
combine_firstgives priority to known values in the caller.- Arithmetic aligns by label;
fill_valuecontrols missing contributions.
4.3 Stack compatible tables with concat
Monthly files usually contain more rows of the same kind of observation. Concatenation collects those pieces into one table. It aligns labels on the other axis; it does not search for matching customer or order key values.
Stack rows and align columns
pd.concat([jan, feb]) stacks rows because the default is axis=0. Suppose January has three trips with columns trip and km, while February has two trips with trip, km, and note. The result has five rows and three columns. January's missing note cells become NaN.
join="outer" is the default: it keeps the union of column names. join="inner" keeps only shared names, so the result has five rows and the two columns trip and km. It still includes all five observations; the join setting controls the other axis.
Column order in the parts does not control the match. Exact names do. If one piece uses km and the other uses Km, the result contains two partly empty columns. Rename the headings before concatenating.
Decide what the row labels should mean
The original indexes survive by default. With starting indexes 0, 1, 2 and 0, 1, the result repeats labels 0 and 1. result.loc[0] selects two rows. Use ignore_index=True when those numbers have no meaning and you want fresh labels 0 through 4.
When source identity matters, use an outer index level:
result = pd.concat(
[jan, feb], keys=["jan", "feb"], names=["month", "row"]
)
Now result.loc["feb"] selects February's two rows. keys preserves origin within a MultiIndex. In pandas 3, combining keys with ignore_index=True raises ValueError; choose source labels or a new plain index for that operation.
Place columns beside each other
pd.concat([x, y], axis=1) aligns rows by index label. For x with a = 1, b = 2 and y with b = 3, c = 4, the result has rows a, b, c. The x column is missing at c, and y is missing at a. With join="inner", only b remains.
| Operation | Added along | Labels aligned on |
|---|---|---|
concat(..., axis=0) | Rows | Column names |
concat(..., axis=1) | Columns | Row indexes |
merge(..., on="id") | Matching row pairs | Key-column values |
Check storage and collect pieces efficiently
A column mixing numeric values and text may become dtype object. If ordinary NumPy int64 data gains a gap, it becomes float64; pandas nullable "Int64" can keep integers with <NA>. Align dtypes before combining when the column has a defined type.
DataFrame.append was removed in pandas 2.0. For many files, collect DataFrames in a list and call pd.concat once after the loop. Rebuilding a growing result in every iteration repeatedly copies accumulated data and becomes slow.
Check that a row concat's length equals the sum of input lengths. Then inspect column names, missing counts, and dtypes.
Key Takeaways
- Row concatenation aligns column names and adds input row counts.
joinchooses shared or all labels on the other axis.- Use
ignore_indexfor disposable labels orkeysfor source identity. - Column concatenation aligns row indexes.
- Collect many parts first and concatenate once.
5Reshape data with hierarchical indexes, pivot and melt
The same measurements can be arranged as rows, columns, or levels of an index. Use a MultiIndex to name observations with several keys, then move those keys with stack and unstack or with pivot and melt. Track which labels identify a cell and whether they are unique. That makes reshaping predictable and lets you tell an absent combination from a measured zero or a pair of records that needs an explicit aggregation.
5.1 Select rows with more than one index level
A MultiIndex gives each row several labels. A sales row might be identified by both city and year, written as a tuple such as ("Osaka", 2024). Each label has a level, and the order of those levels matters for selection.
Build and name the index
df.set_index(["city", "year"]) moves those columns into the index. pd.MultiIndex.from_product([["Lima", "Osaka"], [2024, 2025]], names=["city", "year"]) builds every combination of the supplied labels. Concatenation with keys also adds a level.
For the examples, sales has these index pairs and values:
| city | year | sales |
|---|---|---|
| Osaka | 2024 | 120 |
| Osaka | 2025 | 135 |
| Lima | 2024 | 80 |
| Lima | 2025 | 95 |
After setting the two index levels, df.index.names is ["city", "year"]. rename_axis(["town", "yr"]) changes the level names without changing their values.
Select an outer label or a complete tuple
df.loc["Osaka"] selects both Osaka rows. The selected city level drops away, leaving 2024 and 2025 as the index. df.loc[("Lima", 2025), "sales"] selects one cell and returns 95.
A single label passed to loc searches the outermost level. df.loc[2024] therefore looks for a city named 2024 and raises KeyError in this table. To select a year across cities, use df.xs(2024, level="year"). It returns Osaka's 120 and Lima's 80, with city as the remaining index.
For a more general selection, df.loc[pd.IndexSlice[:, 2025], :] keeps all cities at year 2025. The colon in the first position means all labels in that level; the final colon selects all columns.
Sort before slicing ranges
Exact-label selection does not need a globally sorted index, but slicing a label range needs appropriate sorting. Use df.sort_index() before df.loc["Lima":"Osaka"]. An unsorted MultiIndex can raise UnsortedIndexError for such a slice.
sort_index() sorts all levels in their current order, so cities come first and years within each city. sort_index(level="year") sorts by the named level. swaplevel("city", "year") puts year in the outer position; follow it with sort_index() to group and order the new arrangement. Swapping the level positions alone does not sort the rows.
Move labels back to columns
reset_index() restores both levels as columns. reset_index(level="year") restores only year and leaves city as the index. These operations let you prepare a table for merging or saving without losing its identifying values.
Columns can also have several levels. A reshape with sales and staff values may produce ("sales", 2024) and ("staff", 2025) column labels. df["sales"] selects all columns under the top-level sales label.
For a total by city, df.groupby(level="city")["sales"].sum() gives Lima 175 and Osaka 255. Use grouping for this summary; the old sum(level="city") argument was removed in pandas 2.0.
Key Takeaways
- A tuple identifies a complete MultiIndex label across levels.
- Single-label
locselection begins at the outermost level. - Use
xsorIndexSliceto select an inner level. - Sort the index before label-range slicing.
- Swap levels to change their roles and reset them to restore columns.
- Use
groupby(level=...)for summaries by an index level.
5.2 Move labels between the index and columns
stack and unstack change where labels sit. Values keep their identifying combination, but the table can expose combinations that had no observation.
Stack column labels into the index
Suppose a wide DataFrame has cities Osaka and Lima as its index and years 2024 and 2025 as its columns. Osaka has values 120 and 135; Lima has 80 and 95. Name the axes city and year to keep their meaning visible.
long = wide.stack() moves the year labels into a new innermost index level. With these single-level columns, the result is a Series containing four values:
("Osaka", 2024)gives 120.("Osaka", 2025)gives 135.("Lima", 2024)gives 80.("Lima", 2025)gives 95.
The city labels repeat because each city now has one row per year. Stacking has not summed the values or created additional measurements.
Unstack an index level into columns
long.unstack() moves the innermost index level, year, into columns and recovers the wide arrangement. long.unstack("city") moves city instead, producing years as rows and cities as columns. A numeric level position such as unstack(0) can select the same level, but a name makes the intention easier to read.
| Operation | Label movement |
|---|---|
stack() | A column level becomes an inner index level |
unstack() | An index level becomes a column level |
If the input has hierarchical columns, stacking one level may leave another level as columns. For columns such as ("sales", 2024) and ("staff", 2024), stack("year") returns a DataFrame with sales and staff columns. Stacking all column levels returns a Series.
Keep missing combinations visible
Now suppose the long Series has no Lima 2025 entry. Unstacking makes a rectangular city-by-year table, so it introduces NaN at that position. Ordinary integer values become floats when this missing cell requires a float missing marker.
unstack(fill_value=0) can fill a newly absent combination with zero. Use it only if absence means zero, such as no sales recorded in a complete sales-event dataset. A missing sensor observation is unknown and should stay missing.
pandas 3 stack() keeps missing cells. Stacking the incomplete 2-by-2 table therefore returns four entries, including NaN. Add .dropna() if you want only the three known values. Removing gaps changes which combinations remain represented, so check the result's length.
Resolve duplicate combinations first
Unstacking needs a single value for each complete index combination. Two entries labeled ("Lima", 2025) compete for the same output cell and raise ValueError: Index contains duplicate entries, cannot reshape.
Use long.index.duplicated() to inspect repeated complete labels, or check the corresponding key columns before setting the index. Decide whether the rows are accidental copies or separate observations that should be combined. Removing or aggregating them is a data decision; unstack does not choose a rule for you.
Key Takeaways
stackmoves column labels inward;unstackmoves index labels outward.- Choose the level explicitly when the default is not the one you need.
- Absent combinations appear as missing cells when widened.
- pandas 3 stacking retains missing values unless you drop them afterward.
- Each complete index combination must be unique for unstacking.
5.3 Move between long and wide tables
In long format, identifying columns accompany one value column. In wide format, measurements are spread across columns, such as one column per year. Choose the layout that makes the next task easy: long tables suit filtering and grouping, while wide tables support side-by-side comparisons.
Pivot unique pairs into cells
A long table with columns city, year, and sales becomes wide with df.pivot(index="city", columns="year", values="sales"). City labels form rows, years form columns, and sales fill the cells. It is equivalent to df.set_index(["city", "year"])["sales"].unstack().
Osaka's 2024 and 2025 sales of 120 and 135 become two cells in one row. Lima's 80 and 95 form the second row. Swapping the index and columns arguments transposes this arrangement. Omitting values pivots all remaining data columns and can create hierarchical columns, such as a sales level and a staff level above the years.
Decide whether repeats are errors or observations
pivot rearranges values and cannot combine them. Two Lima 2025 rows would require two values in one output cell, so they raise ValueError. Count repeated pairs with df.duplicated(subset=["city", "year"]).sum().
If separate observations should be combined, use pivot_table with a chosen aggregation. For values 95 and 105 at Lima 2025, its default mean gives 100.0. With aggfunc="sum", the cell is 200. If each pair should have appeared once, keep pivot so it reveals a data error instead of silently summarizing it.
Melt measurement columns into rows
Suppose a CSV gives columns city, 2023, 2024, and 2025. Osaka's values are 110, 120, 135; Lima's are 70, 80, 95. Melt the year columns:
long = wide.melt(
id_vars="city", var_name="year", value_name="sales"
)
id_vars stays in place and repeats. Each measurement-column label becomes a year value; each cell becomes a sales value. Two rows and three measurement columns produce six long rows, including one such as Osaka, "2023", 110. Missing measurement cells also become rows unless you drop them afterward.
Because these column labels came from CSV text, the resulting years are text. Use long["year"] = long["year"].astype(int) before doing arithmetic with them. Integer input column labels would already give integer year values.
Control what gets melted and restore the shape
value_vars=["2024", "2025"] limits melting to those columns. Forgetting id_vars can melt city names into the same value column as measurements. Always distinguish identifiers from the columns that contain values.
Without explicit names, the value column is called value. The variable column inherits wide.columns.name, or is called variable if that name is empty. Explicit var_name and value_name make the result easier to use.
Pivot the six rows back with index="city", columns="year", values="sales" to restore the three year columns. After pivoting, city is the index and the column axis is named year. reset_index() restores city as a column, and rename_axis(columns=None) removes the column-axis name. Verify both the shape and the labels before merging or saving the table.
Key Takeaways
- Long format puts measurement labels in rows; wide format puts them in columns.
pivotneeds unique row/column pairs and performs no aggregation.pivot_tablecombines repeated pairs using the chosen aggregation.meltrepeats identifiers and creates one row per measurement cell.- Check column-label types when they become values.
- Reset indexes and axis names when a flat output table is needed.
Wrangling Data with pandas examples
1Clean readings without losing their meaning
16 steps
- 1IntroductionDecide what each gap means
A weather table has four cities and two measured columns: temperature in °C and rainfall in mm. Some readings are missing. You will locate those gaps, decide which rows are usable, and distinguish a repeated record from a genuinely different observation. The useful result is a table whose remaining values still mean what you think they mean. Use
import pandas as pdandimport numpy as npfor the code. - 2ExplanationRecognize the missing marker
A gap can appear as
NaN,NaT, orpd.NA, depending on its dtype.Noneis also recognized as missing.Use
df.isna()to make a Boolean table withTrueat each gap. Comparing withnp.nanmisses those values because NaN is unequal even to itself. The detection method lets the same check work across numeric, text, and datetime columns. - 3ExampleCount the weather gaps
Lima has temperature 18 and rainfall 0. Oslo lacks temperature but has rainfall 4. Pune has temperature 31 and lacks rainfall. Kyiv lacks both readings. Every city name is known.
df.isna().sum()gives city 0, temperature 2, and rainfall 2.df.isna().mean()gives missing shares of 0, 0.5, and 0.5 because each column has four entries. - 4Key InsightDistinguish cells from rows
Kyiv has two missing cells, while Oslo and Pune have one each. There are four missing cells but only three affected rows.
df.isna().sum().sum()counts the cells.df.isna().any(axis=1).sum()first marks whether each row has any gap, then counts those rows. The row-wiseanyprevents Kyiv from being counted twice. - 5ChallengeFind a hidden missing code
A sensor column contains 0, 7, -999, and
NaN. Its documentation says -999 means the sensor was off. How many values should remain known after cleaning?---reveal---
Two: 0 and 7. Replace the documented -999 code with a missing marker, then use
isna()again. Zero is a genuine reading here. Treating every zero as missing would remove a known value along with the unavailable readings. - 6ExampleKeep rows that have a temperature
Suppose the analysis needs temperature but can tolerate unknown rainfall. Use
clean = df.dropna(subset=["temp_c"]). This keeps Lima and Pune.The default
dropna()would keep only Lima because it checks every column.thresh=2would also retain Oslo, whose city and rainfall are known. Choose the rule that expresses the columns the analysis actually needs. - 7ExampleMark estimates before filling
For a rainfall analysis that keeps all cities, start from the original four rows. Before filling temperatures, save
df["temp_missing"] = df["temp_c"].isna().The known temperatures 18 and 31 average to 24.5. Assign
df["temp_c"] = df["temp_c"].fillna(df["temp_c"].mean())to fill the two gaps. The saved indicator still identifies Oslo and Kyiv as estimated readings. - 8TipChoose the estimate by its meaning
A mean fill uses the known readings as a simple estimate, but it reduces apparent variation. The median is less sensitive to extreme readings. Filling with zero states something different: the missing quantity was zero.
A switched-off rain sensor does not establish zero rainfall. Use per-column fills compatible with their dtypes, such as a dictionary with numeric values for numeric fields and text for text fields.
- 9ConnectionUse order only when it carries information
Forward filling carries the previous known value into a gap; backward filling uses the next. With 12,
NaN,NaN, 15,ffill(limit=1)gives 12, 12,NaN, 15.Interpolation instead estimates between known positions: 10,
NaN,NaN, 16 becomes 10, 12, 14, 16. These methods fit meaningfully ordered readings, not a list of unrelated cities. - 10ChallengeKeep usable rows without claiming zero rain
Three records contain A/19/2, B/21/
NaN, and C/NaN/5 for city/temperature/rainfall. You need temperature comparisons and cannot estimate missing rain. What rows and values should you retain?---reveal---
Keep A and B by checking the temperature column with
dropna(subset=["temp_c"]). Leave B's rainfall missing. Dropping on every column would lose B's usable temperature; filling its rainfall with zero would add an unsupported measurement. - 11ExampleCompare complete records
An orders table has IDs 101, 102, 102, 103, 104, 104. The two 102 rows are identical. The two 104 rows differ in recorded weight: 0.5 kg and 0.8 kg.
df.duplicated().sum()reports one extra full-row copy.drop_duplicates()leaves five rows because the differing 104 records are not exact copies. Index labels do not affect this comparison. - 12ExplanationChoose a key and a retained version
Comparing only
order_idmarks the second 102 and the second 104 as repeats, giving two extra key copies. The question is now which version represents each order.keep="last"chooses the last in the current row order. To make that mean newest, sort by an update field first.keep=Falseremoves all members of repeated groups, so it would discard both 104 records. - 13ChallengeKeep the newest record
Item H has updates 1, 4, 2 and weights 3, 6, 5 kg in that input order. How do you retain its newest record, and what weight survives?
---reveal---
Sort
updatedascending, then usedrop_duplicates(subset="item", keep="last"). The latest update is 4, so 6 kg survives. Keeping the last input row without sorting would select update 2 and incorrectly retain 5 kg. - 14TipStandardize before checking identity
Text keys " Lisbon", "lisbon", and "LISBON " do not match exactly. Trim and lowercase them when those spellings identify the same city. Then inspect repeated keys.
The table's purpose decides whether repetition is valid: many orders may share a customer, but a lookup intended to give one detail row per customer needs unique keys. Removing valid orders cannot repair a repeated lookup key.
- 15ChallengeLink text cleaning to record choice
Customer records have keys " emi " and "EMI", updates 2 and 5, and balances 10 and 14. The older balance is missing in a third unrelated record. You need the latest record per customer. What should happen before any balance filling?
---reveal---
Standardize the keys, sort by update, and keep the last row per cleaned key. The two Emi spellings become one customer with balance 14. Resolve record identity first so obsolete versions do not influence a later fill statistic.
- 16RecapCheck the result against the decision
After cleaning, compare the row count, missing counts, and key uniqueness with the result you intended. A missingness indicator keeps estimates visible; a sorted key-based duplicate check keeps the chosen record version visible.
Save returned objects through direct assignment. Under pandas 3 Copy-on-Write, a chained
inplacecall on a selected column does not update its parent DataFrame.
2Transform codes and measurements
15 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionChoose the meaning of the output
- 2ExampleTranslate or edit a value
- 3ExampleConvert the text after cleaning its format
- 4Key InsightKeep headings separate from cells
- 5ChallengeLocate the source of a new gap
- 6ExampleRead the ends of an interval
- 7ExplanationInclude the lowest edge deliberately
- 8ConnectionChoose widths or counts
- 9ChallengeTest the boundary rule
- 10ExampleSubtract the minimum before dividing
- 11ExampleMeasure distance from the mean
- 12TipCap without deleting an observation
- 13ExplanationRepresent category membership
- 14ChallengeExplain an all-false indicator row
- 15RecapVerify the representation you created
3Turn inconsistent text into useful labels
17 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionGive text a consistent structure
- 2ExplanationApply a method to every string
- 3ExampleSplit only where the format requires
- 4TipKeep a plain-text search literal
- 5ChallengePrepare a text merge key
- 6ExplanationBuild a quantity pattern
- 7ExampleExtract the first quantity
- 8ConnectionChoose all matches when one is insufficient
- 9ChallengeReject a partial code match
- 10TipLimit what a pattern consumes
- 11ExampleSeparate the label list from its codes
- 12ExplanationGive sizes their intended order
- 13TipMake room before assigning a new label
- 14ChallengeKeep empty categories visible
- 15ConnectionCombine categories across batches
- 16ChallengeClean before defining the allowed list
- 17RecapInspect both text values and storage
4Combine tables and account for every row
18 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionChoose how the tables relate
- 2ExampleName the shared key explicitly
- 3ExplanationPreserve the side you need
- 4Key InsightExpect every matching pair
- 5TipMake the relationship a checked rule
- 6ExampleCount unmatched keys separately
- 7ChallengeFind the cause of extra rows
- 8ExampleMatch a left column to a right index
- 9TipDistinguish overlapping columns
- 10ExampleLet the main source take priority
- 11ConnectionFollow labels when adding values
- 12ChallengePatch the correct observation
- 13ExampleAdd observations by matching column names
- 14ExplanationKeep a source label or make a fresh index
- 15Key InsightAlign rows when adding columns
- 16ChallengeDiagnose new distance gaps
- 17ChallengeCombine batches before adding lookup details
- 18RecapAccount for additions and matches
5Reshape values while keeping their labels
18 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionTrack the identity of each measurement
- 2ExampleGive a row two labels
- 3ExplanationSelect the level you actually mean
- 4TipSort after changing level roles
- 5ChallengeSelect an inner level without changing the data
- 6ExampleMove year columns into the index
- 7ExplanationChoose the level that becomes columns
- 8Key InsightRecognize a newly visible gap
- 9ChallengeFind why widening fails
- 10ConnectionChoose long or wide for the next task
- 11ExamplePivot a unique pair into a cell
- 12ExplanationAggregate only when it is intended
- 13ExampleMelt measurements while keeping identifiers
- 14TipCheck what type a column label becomes
- 15ChallengeReconstruct a missing wide cell
- 16ConnectionRestore a table ready for column operations
- 17ChallengeChoose the repair for repeated measurements
- 18RecapCheck shape, uniqueness, and meaning
Wrangling Data with pandas practice questions
Set 1: Read the data and predict simple changes10 Practice Questions
Question 1 of 10
NaN, and 12. What is its default mean?Show answerHide answer
len counts positions; the default mean skips gaps.Question 2 of 10
df.isna().any(axis=1).sum() count?Show answerHide answer
Question 3 of 10
item, mass_kg, and note. Row U has a known mass and a missing note; row V has a missing mass and a known note. Both item names are known. What does df.dropna(subset=["mass_kg"]) retain?Show answerHide answer
Question 4 of 10
s.map({"A": "active", "B": "blocked"}) produce?Show answerHide answer
replace with this dictionary. A dictionary passed to map defines outputs for the whole mapping, not just edits to selected values.Question 5 of 10
pd.cut(x, [0, 10, 20]) with defaults. Where does 10 go?Show answerHide answer
right=False.Question 6 of 10
s.str.strip().str.lower(), how many distinct values and rows remain?Show answerHide answer
Question 7 of 10
str.contains(r"\d+") and str.fullmatch(r"\d+") classify it?Show answerHide answer
Question 8 of 10
Show answerHide answer
Question 9 of 10
id, km. Three-row batch B has columns id, km, flag. What shape results from pd.concat([a, b], ignore_index=True)?Show answerHide answer
join="inner"; resetting the index does not discard an extra column.Question 10 of 10
station, dry, and wet. What length results from melting the two measurement columns with id_vars="station"?Show answerHide answer
Set 2: Choose methods for common wrangling tasks10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
NaN, NaN, 11.0. What does ffill(limit=1) produce?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
drop_duplicates(subset="id", keep=False)?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
pd.to_numeric(..., errors="coerce"), what values remain?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
s.replace("Rd", "Road")?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
NaN; Series b has q = 9, r = 2. What is a.combine_first(b)?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
.loc["C"] to obtain totals by site for month 6. What correction preserves the readings and gives the needed totals?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
Show answerHide answer
Save this lesson to see this answer.
Set 3: Trace labels, keys, and changing shapes10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
qcut groups?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
s.str.extract(r"(\d+)\s*kg", expand=False) followed by numeric conversion return?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
"", " ", and None, created directly in memory. How many values does s.isna().sum() count?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
["small", "large"], then counted with dropna=False. What counts appear?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
_merge row counts for both, left_only, and right_only?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
x.add(y, fill_value=0), what is the sum of the resulting values?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
swaplevel("city", "year").sort_index(), what does .loc[2027] select?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
pivot_table using site as rows, year as columns, and default aggregation, then melting the one year column?Show answerHide answer
Save this lesson to see this answer.
Set 4: Diagnose changes in a cleaning pipeline10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
NaN. You save a missingness indicator, then fill the gap with the column median. What value is filled and how many indicator values are true?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
None, " 9 ". After to_numeric(errors="coerce"), how many originally present values have become missing?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
cut with edges [0, 10, 20] and default indicators, two rows are all-false. You need valid masses categorized and unknown readings separately identifiable. What repair fits?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
s.str.split("/", n=1, expand=True) do to "zone/aisle/bin"?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
r"(\d+)" and convert them. What numeric values remain?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
id, km. Table b has one row with columns id, Km. After row concatenation with default column handling, how many cells are missing in the two distance columns combined?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
.loc[2026] and then pivots. What correction selects the data and produces a single report row?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
site, day, and night. One night cell is missing. After melting both measurements, dropping missing values, and pivoting back, what are the long row count and wide missing-cell count?Show answerHide answer
Save this lesson to see this answer.
Set 5: Reason through linked transformations10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
NaN, and 6. Its documentation defines -99 as missing and 0 as a real reading. After cleaning, what are the known count and mean?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
{"a": "4", "b": "bad"} and then applies numeric coercion. Which original entries finish missing, and why?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
NaN; backup values are a = 50, b = 7, c = 9. After primary.combine_first(backup), what would reversing source priority change?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
kg and count, both using nullable "Int64". A third has only kg with that dtype. After row concatenation, what best explains the resulting count column?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
Show answerHide answer
Save this lesson to see this answer.
Stuck on your own question? Homework Help
Key Terms
A
Axis
B
Backward fill
Bin
(0, 10], the measurement 10 is included and the measurement 0 is excluded.C
Capping
Capture group
Case normalization
Categorical dtype
Category code
Category union
Character class
\d or a range such as [A-Z] in a pattern.[A-Z] matches one uppercase letter in an inventory code before its digits.Coercion
to_numeric converts readable values to numbers and turns unreadable entries into missing values instead of raising.combine_first
combine_first to patch missing measurements from a backup while keeping known values from the main source.Concatenation
Copy-on-Write
Cross-section
D
dropna
dropna(subset=["mass_kg"]) when usable rows must have a mass even if another field is incomplete.Duplicate row
F
Forward fill
Full match
r"\d+" accepts "204" and rejects "204x", even though both strings contain digits.G
Greedy repetition
r"<.*>" can cover both tags and the text between them in a short marked-up string.I
id_vars
id_vars="station" keeps station names out of the numeric readings created by a melt.ignore_index
ignore_index=True when monthly files have disposable row numbers that would otherwise repeat.Imputation
Index join
join uses the right index even when a left key column is specified.Index level
loc selection.Index sorting
Indicator column
get_dummies.Inner join
Interpolation
NaN, 12 into 8, 10, 12 when a straight-line estimate fits.L
Label alignment
Left join
Literal matching
Long format
M
Many-to-one relationship
map
map exposes codes absent from the dictionary as gaps that the analyst can inspect.melt
melt of four year columns creates four rows per city, including rows for empty measurement cells.Merge
Merge indicator
Min-max scaling
Missing combination
NaN when the long readings are unstacked.Missing value
rain_mm means the reading is unavailable, so it must be distinguished from no rain.Missingness mask
MultiIndex
N
NaN
isna() to find NaN in the temperature column before counting the readings available for a mean.NaT
isna() detects it alongside missing numeric readings.Nullable dtype
"Int64" for integers with pd.NA.O
Ordered categorical
Outer join
P
pivot
pivot of station and month requires one reading for each station/month cell in the output.pivot_table
pivot_table with aggfunc="sum" when separate shipments should add into a daily total.Q
Quantile binning
qcut to compare low, middle, and high groups containing similar numbers of readings.R
Raw string
r that preserves backslashes for uses such as regular-expression patterns.r"\d+", so its backslash reaches the regex engine.Record key
Regular expression
rename
rename(columns={"Weight": "mass_kg"}) to give a heading a clear meaning without editing its cells.replace
replace dictionary can change "pending" to "open" without altering the other status values.reset_index
reset_index(level="year") to restore year as a column while leaving city in the index.Reshaping
S
Source key
concat becomes an outer index label identifying the part from which each row or column originated.stack
stack on a city table with year columns gives city/year index pairs for the same sales values.str accessor
s.str.lower() to lowercase an entire city column in one expression.String splitting
subset
subset="order_id" to detect repeated order keys even when their recorded weights differ.Suffix
_sensor keeps one reading column distinct from the same-named column in a backup table.T
Type conversion
U
Uniqueness
unstack
unstack("year") to put each year into a separate sales column for every city.Unused category
V
Vectorized operation
W
Whitespace
str.strip().Wide format
Z
Z-score
About this lesson
Make your own Study Materials
Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.