Pandas Drop Duplicates by Column and Keep the Latest Row
Deduplicate records by email while retaining the latest timestamped row and leaving the input unchanged.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Pandas Drop Duplicates by Column and Keep the Latest Row
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
A contact import contains two rows for one email address. The newer update appears first, and the older update appears second. We want to keep the latest contact, but two plausible one-line fixes give the wrong result. Calling `drop_duplicates()` with no arguments compares every column, so both rows remain because their names or timestamps differ. Adding `subset=['email']` identifies the duplicate, but `keep='last'` means last in current row order, not latest in time. If we use it directly on this sample, we keep the old record. This lesson answers the specific search phrase pandas drop duplicates by column
- 00:40
while making the choice of surviving row explicit. We will first expose the full-row default, then define email as the identity key, parse and sort update times, and verify ties and missing values. The browser uses a small in-memory DataFrame; local Python needs `python -m pip install pandas`. The default removes exact duplicate rows, and these rows are not exact duplicates because name differs. The subset argument changes the identity rule: only email is considered when deciding whether two rows are duplicates. It does not delete the email column from the result.
- 01:20
This is a simple but important distinction. Before removing records, decide whether email is truly the right unique key for your product. Shared email accounts, aliases, case differences, and missing addresses can all change the answer. Our classroom example assumes one contact per exact email string. A real import might normalize case or whitespace, but that is a separate business rule and should not happen silently. We also need to decide which of the two rows survives. Pandas defaults to the first observed row; `keep='last'` chooses the last observed row. Neither option automatically reads the timestamp.
- 02:01
Let's make that failure visible with the newer row intentionally placed first. The method has done exactly what `keep='last'` asks: it kept the second input row. But that row is older. The phrase latest is not built into `drop_duplicates`; we must define order ourselves. Parse `updated_at` with `pd.to_datetime(..., utc=True)` rather than assuming a mixed set of date strings will sort chronologically as text. Then sort by that parsed value. Use a stable sort so two equal timestamps keep their original order. In our policy, the later input row wins a tie. If that is not acceptable, add another explicit tie breaker such as a version
- 02:43
number. The helper will add a temporary parsed- time column, sort, drop duplicate email values with `keep='last'`, reset the index, and remove that temporary column. This produces a new DataFrame and leaves the source untouched. A new table is safer for a teaching example because it makes the transformation inspectable before replacing an imported dataset. The parsed timestamps put the old row before the new row, then `keep='last'` selects the new one. `ignore_index=True` gives the result a clean sequence of row labels, which helps when later code reads or displays it.
- 03:24
The temporary parsed-time column does not leak into the return value. We reject a missing email because otherwise missing identity values may be treated as duplicate candidates and an unrelated contact could disappear. This is our chosen policy, not a claim that pandas itself rejects missing keys. For a production import, inspect rows before dropping them and decide whether to preserve a review log. If one duplicate row has a useful phone number missing from the newer row, deduplication by selection will lose that field. Combining fields is a different reconciliation problem. The function does not solve it automatically.
- 04:06
Also note that emails with different capitalization remain different strings here. Normalize only if your application has an explicit rule for that comparison. For the equal timestamps, stable sorting preserves first then second, so keeping the last selects second. That tie rule is predictable but only appropriate if input order has meaning. `keep=False` implements a different policy: it discards every row whose email occurs more than once, leaving only the unique b record. `duplicated` gives a Boolean mask to inspect those rows before removing them. In a real data cleanup, that mask can feed a review report, while the retained result
- 04:47
can feed the application. Check that the input frame remains unchanged and test an empty or missing email according to your contract. On the lesson page, fix the starter function, run the reverse-order and tie tests, and explain why `subset` and `keep` solve different parts of the problem. The core lesson is to define identity and survivor order separately. A one-line deduplication call is only correct after both decisions are made.
Understand the concept
By default, DataFrame.drop_duplicates compares all columns. If two rows share an email but differ in a name or update time, neither is a full-row duplicate. For the query 'pandas drop duplicates by column', the key parameter is subset=['email']. Specify keep='first', keep='last', or keep=False according to which records should survive. The default first means first in the current row order, not chronologically earliest.
To keep the latest record per email, parse updated_at with pd.to_datetime(..., utc=True), sort by that timestamp with a stable sort, then drop_duplicates(subset=['email'], keep='last'). A stable sort preserves original order for exact timestamp ties; therefore the later input row wins a tie. The output order is timestamp order. If another order is required, sort again with a documented key. ignore_index=True produces a clean sequential index for the returned table.
The task rejects missing email values, because pandas otherwise treats missing keys together during duplicate selection and may discard unrelated unknown contacts. It returns a new DataFrame without the temporary parsed-time column and without changing the source. Real data may have whitespace or case variants such as Ada@example.com and ada@example.com; decide whether to normalize them before deduplication. Do not silently normalize if case distinctions matter to your application.
Install pandas in local Python with `python -m pip install pandas`; the browser examples load the Pyodide package and use only tiny in-memory fixtures. The checks cover reversed input order, exact timestamp ties, missing keys, and an unchanged source. drop_duplicates is a selection rule, not a merge of conflicting data: if one row has a useful field missing from the newer row, that information will be lost unless you design a separate reconciliation policy.
- subset chooses duplicate identity columns
- keep='last' refers to current row order
- Stable timestamp sorting defines a reproducible latest rule
- Missing identity keys need an explicit policy
See it step by step
Read the code, predict the output, then compare it with the result.
01. Compare full rows with one identity column
import pandas as pd
contacts = pd.DataFrame({'email':['a@example.com','a@example.com'], 'name':['Ada','Ada B']})
print(len(contacts.drop_duplicates()))
print(len(contacts.drop_duplicates(subset=['email'])))2
1The two rows differ in name, but email alone defines identity in this example.
02. Keep the chronologically latest row
import pandas as pd
contacts = pd.DataFrame({'email':['a@example.com','a@example.com'], 'updated_at':['2024-02-02','2024-02-01'], 'name':['new','old']})
ordered = contacts.assign(_time=pd.to_datetime(contacts['updated_at'], utc=True)).sort_values('_time', kind='stable')
print(ordered.drop_duplicates(subset=['email'], keep='last')['name'].tolist())['new']The newer row survives even though it appeared first in the input.
03. Choose a different duplicate policy
import pandas as pd
rows = pd.DataFrame({'email':['a','a','b'], 'name':['first','second','only']})
print(rows.drop_duplicates(subset=['email'], keep=False)['name'].tolist())
print(rows.duplicated(subset=['email'], keep=False).tolist())['only']
[True, True, False]keep=False removes every row belonging to a duplicate identity; duplicated identifies those rows before removal.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Choose the identity column
A default drop_duplicates call compares all columns. Two entries with the same email and different names are different full rows, so both remain. This behavior is correct for exact duplicate rows but not for a one-contact-per-email rule.
Pass subset=['email'] to define identity explicitly. A compound identity could use two column names, but this example has one. Always record why that field identifies a record; otherwise later input changes can make the deduplication rule destructive.
import pandas as pd
contacts = pd.DataFrame({'email':['a@example.com','a@example.com'], 'name':['Ada','Ada B']})
print(len(contacts.drop_duplicates()))
print(len(contacts.drop_duplicates(subset=['email'])))2
1- Run both duplicate counts.
- Name the row difference that prevents default deduplication.
Define what latest means
keep='last' keeps the last occurrence in the table's current order; pandas does not infer that a column named updated_at is a timestamp. Parse it with pd.to_datetime and utc=True, sort stably, and only then choose the last row per email.
Stable sorting gives a deterministic tie policy: equal timestamps retain input order, so the later input row survives. If that is not the desired tie breaker, add an explicit sequence number or other business key. The task also resets the result index and drops its temporary parsed timestamp column.
import pandas as pd
contacts = pd.DataFrame({'email':['a@example.com','a@example.com'], 'updated_at':['2024-02-02','2024-02-01'], 'name':['new','old']})
ordered = contacts.assign(_time=pd.to_datetime(contacts['updated_at'], utc=True)).sort_values('_time', kind='stable')
print(ordered.drop_duplicates(subset=['email'], keep='last')['name'].tolist())['new']- Run the reversed-time input.
- Explain why a raw keep='last' call would choose old instead.
Inspect discarded rows and null keys
Use duplicated(subset=['email'], keep=False) to see every member of a duplicate set before deleting anything. keep=False is another policy: it discards every duplicated identity rather than choosing one. For review workflows, export or log the discarded rows before finalizing a dataset.
Missing email values need their own rule. In this lesson they are rejected rather than grouped as one unknown person. The function returns a new DataFrame and does not mutate input; a real import should also decide whether case and surrounding whitespace are normalized before comparison.
import pandas as pd
rows = pd.DataFrame({'email':['a','a','b'], 'name':['first','second','only']})
print(rows.drop_duplicates(subset=['email'], keep=False)['name'].tolist())
print(rows.duplicated(subset=['email'], keep=False).tolist())['only']
[True, True, False]- Run keep=False and duplicated on the same data.
- Describe which rows would be lost and whether that matches the intended policy.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Reject frame['email'].isna().any(); assign a parsed UTC timestamp column, sort with kind='stable', drop_duplicates(subset=['email'], keep='last', ignore_index=True), then drop the temporary column.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Dropping only full-row duplicates
frame.drop_duplicates()frame.drop_duplicates(subset=['email'])What happens: Two rows with one email remain when their timestamps differ.
Specify the column that defines duplicate identity.
Keeping the last input row without sorting
frame.drop_duplicates(subset=['email'], keep='last')frame.sort_values('_parsed_time', kind='stable').drop_duplicates(subset=['email'], keep='last')What happens: A newer row appearing earlier can be discarded.
Define chronological order before choosing the last row.