Before you analyze anything, you need to orient yourself in your data. This lesson teaches you the five pandas methods every analyst uses in the first two minutes with a new dataset — and how to combine them into a fast, reliable exploration workflow.

You've just loaded a dataset into pandas. Maybe it's 200,000 rows of sales transactions, a year of customer orders, or a dump from a production database. Now what? You can't scroll through 200,000 rows the way you'd skim an Excel sheet. You need a smarter way to look at your data — quickly, confidently, and without getting overwhelmed.
This is exactly the problem that a handful of deceptively simple pandas methods solve. Before you filter, clean, aggregate, or visualize anything, you need to inspect your data: peek at the top and bottom rows, take a random sample to spot-check quality, sort to find your biggest or smallest values, and rank your top performers. These aren't glamorous operations, but they're the ones experienced analysts reach for within the first 30 seconds of touching a new dataset. Get comfortable with them and you'll develop instincts that catch problems early and guide your analysis in the right direction.
By the end of this lesson, you'll be able to confidently orient yourself in any new DataFrame, surface the rows that matter most, and sort your data exactly the way you need it — all before writing a single line of complex analysis code.
What you'll learn:
head() and tail()sample() to spot-check data qualitysort_values()nlargest() and nsmallest()This lesson assumes you're comfortable with what a pandas DataFrame is and how to load data into one. If you're just getting started, work through Your First pandas DataFrame: Loading CSV and Excel Files and Exploring Data first. You should also have Python and pandas installed — if not, Setting Up Python for Data Analysis: Python, VS Code, Jupyter, and Virtual Environments will walk you through it.
Throughout this lesson, we'll work with a realistic sales dataset. Let's build one directly in Python so you can follow along without needing an external file:
import pandas as pd
import numpy as np
np.random.seed(42)
regions = ['North', 'South', 'East', 'West']
products = ['Laptop', 'Monitor', 'Keyboard', 'Mouse', 'Headset', 'Webcam']
reps = ['Alice', 'Bob', 'Carol', 'David', 'Eve', 'Frank']
n = 500
df = pd.DataFrame({
'date': pd.date_range('2023-01-01', periods=n, freq='D').strftime('%Y-%m-%d'),
'rep': np.random.choice(reps, n),
'region': np.random.choice(regions, n),
'product': np.random.choice(products, n),
'units': np.random.randint(1, 50, n),
'unit_price': np.random.choice([299.99, 499.99, 89.99, 29.99, 149.99, 79.99], n),
'discount_pct': np.random.choice([0, 5, 10, 15, 20], n),
})
df['revenue'] = (df['units'] * df['unit_price'] * (1 - df['discount_pct'] / 100)).round(2)
# Sprinkle in a few missing values to make it realistic
df.loc[np.random.choice(df.index, 15), 'discount_pct'] = np.nan
We now have 500 rows of sales data across multiple reps, regions, and products — realistic enough to make the examples meaningful.
head() returns the first N rows of a DataFrame. By default, N is 5, but you can pass any number you want.
df.head()
date rep region product units unit_price discount_pct revenue
0 2023-01-01 David West Monitor 14 499.99 0.0 6999.86
1 2023-01-02 Carol North Webcam 23 79.99 5.0 1747.78
2 2023-01-03 Eve East Laptop 41 299.99 10.0 11099.63
3 2023-01-04 Alice South Mouse 36 29.99 20.0 863.71
4 2023-01-05 Bob West Headset 18 149.99 15.0 2294.85
df.head(10) # First 10 rows
This gives you an immediate feel for the columns that exist, what data types look plausible, and whether the data loaded correctly. Think of it as the pandas equivalent of pressing Ctrl+Home in Excel to jump to the top of your spreadsheet.
tail() works identically, but shows you the last N rows:
df.tail()
date rep region product units unit_price discount_pct revenue
495 2023-05-12 Frank West Keyboard 33 89.99 0.0 2969.67
496 2023-05-13 Alice East Webcam 12 79.99 20.0 767.90
497 2023-05-14 Carol North Mouse 28 29.99 10.0 755.75
498 2023-05-15 Bob South Laptop 7 299.99 5.0 1994.93
499 2023-05-16 David West Monitor 42 499.99 15.0 17849.64
Why bother checking the tail? Because the end of a file often reveals problems that the top doesn't: truncated imports, summary rows accidentally included in the data (a classic Excel gotcha), or data that stops partway through a time period. Always check both ends.
Tip
When loading data from an external source, check both df.head() and df.tail() before doing anything else. A mismatched row count or a weird trailing row is much easier to catch now than after you've already built analysis on top of it.
head() and tail() only show you the edges of your dataset. For large datasets, the rows at the top and bottom are often not representative — they might all be from the same date, region, or customer. That's where sample() comes in.
sample() returns a random selection of rows from your DataFrame. It doesn't care about order — it just grabs rows at random, which makes it a great spot-check tool.
df.sample(5)
date rep region product units unit_price discount_pct revenue
312 2023-11-09 Alice East Webcam 17 79.99 10.0 1223.85
88 2023-03-30 Bob North Mouse 45 29.99 5.0 1282.07
441 2023-02-27 Carol South Keyboard 11 89.99 NaN 989.89
201 2023-07-20 Frank West Laptop 29 299.99 0.0 8699.71
67 2023-03-08 David South Headset 39 149.99 15.0 4962.67
Notice that row 441 shows a NaN in discount_pct — that's exactly the kind of data quality issue sample() helps surface. If you only ever looked at head(), you might miss that missing values exist at all.
One frustration with random sampling: every time you run sample(), you get different rows. That's fine for exploration, but if you want reproducible results — for example, to share a specific sample with a colleague — use the random_state parameter:
df.sample(5, random_state=99)
With the same random_state, you'll get the same rows every time. Think of it as setting a seed for randomness.
Sometimes you don't want a fixed number of rows — you want a percentage. Use frac:
df.sample(frac=0.1) # Random 10% of the dataset (50 rows from our 500)
This is especially useful when you're working with a large dataset and want to prototype your analysis on a manageable slice before running it on everything.
Note
sample() without random_state is non-deterministic — meaning you'll get different results every run. For exploratory work, that's fine. For anything you need to reproduce or share, always set random_state.
Sorting in pandas works differently than sorting in Excel, but it's more powerful. In Excel, sorting rearranges your sheet permanently (unless you undo). In pandas, sort_values() returns a new sorted DataFrame — your original is untouched unless you explicitly tell pandas to modify it.
df.sort_values('revenue')
By default, this sorts ascending — lowest revenue first. To get the highest revenue first, use ascending=False:
df.sort_values('revenue', ascending=False)
date rep region product units unit_price discount_pct revenue
499 2023-05-16 David West Monitor 42 499.99 15.0 17849.64
396 2023-02-01 Eve North Monitor 49 499.99 0.0 24499.51
...
Key insight
sort_values() returns a new DataFrame — it doesn't modify df in place. If you want to sort and keep using that sorted version, assign it: df_sorted = df.sort_values('revenue', ascending=False). Or add inplace=True, but in practice, assigning to a new variable is cleaner and less confusing.
This is where pandas pulls ahead of basic Excel sorting. You can sort by multiple columns at once by passing a list. Rows are sorted by the first column, then ties are broken by the second column, and so on.
Suppose you want to see each sales rep's transactions, and within each rep, you want the most recent transaction first:
df.sort_values(['rep', 'date'], ascending=[True, False])
date rep region product units unit_price discount_pct revenue
496 2023-05-13 Alice East Webcam 12 79.99 20.0 767.90
489 2023-05-06 Alice South Mouse 32 29.99 5.0 911.69
482 2023-04-29 Alice West Laptop 5 299.99 10.0 1349.96
...
ascending takes a list that matches the columns list — True for ascending, False for descending.
By default, pandas sorts NaN values to the end of a sorted column, regardless of sort direction. That behavior is usually fine, but you can override it:
df.sort_values('discount_pct', ascending=False, na_position='first')
# NaN rows appear at the top
This is useful when you specifically want to investigate rows with missing values — put them first so they're visible immediately.
Warning
Forgetting that sort_values() returns a new DataFrame is one of the most common pandas mistakes. If you write df.sort_values('revenue', ascending=False) and then on the next line do df.head(10), you'll see the original unsorted top 10 — not the top 10 by revenue. Always either chain the methods or save the result to a variable.
If all you want is the top N rows by a numeric column, nlargest() is more convenient than sort_values() + head(). It's also slightly faster on large DataFrames.
df.nlargest(10, 'revenue')
This returns the 10 rows with the highest revenue values, already sorted from highest to lowest. No need to chain ascending=False and then head(10).
df.nsmallest(5, 'units')
This returns the 5 rows with the lowest units values — handy for finding your smallest orders or under-performing transactions.
When values are tied, you can pass additional columns to break the tie:
df.nlargest(10, ['revenue', 'units'])
Pandas will return the 10 rows that are largest by revenue, breaking ties with units.
Tip
Use nlargest() and nsmallest() when you just want to find extremes quickly. Use sort_values() when you need full control — multiple sort columns with mixed ascending/descending, reuse of the sorted DataFrame, or sorting non-numeric columns like dates or text.
Here's a slightly more advanced pattern. What if you want the top 3 transactions for each sales rep? You can combine groupby() with apply() and nlargest():
top3_per_rep = (
df.groupby('rep', group_keys=False)
.apply(lambda g: g.nlargest(3, 'revenue'))
)
top3_per_rep
This gives you 3 rows per rep — the 3 biggest deals each person closed. This kind of grouped ranking is a building block for leaderboards and performance reports. Once you're comfortable with the basics, explore Grouping and Aggregating in pandas: groupby as the PivotTable Replacement to go deeper on this pattern.
Let's walk through what a realistic first-look exploration looks like when you receive a new dataset. This is the actual sequence experienced analysts use:
import pandas as pd
import numpy as np
# Step 1: Load the data (or use our in-memory DataFrame)
# df = pd.read_csv('sales_2023.csv')
# Step 2: Understand the shape and structure
print(df.shape) # (500, 8) — 500 rows, 8 columns
print(df.dtypes) # Column types
print(df.columns.tolist()) # Column names
# Step 3: Peek at the top and bottom
print(df.head())
print(df.tail())
# Step 4: Take a random sample to spot-check middle of the data
print(df.sample(10, random_state=1))
# Step 5: Check for missing values
print(df.isnull().sum())
# Step 6: Sort to see biggest transactions — where's the money?
print(df.sort_values('revenue', ascending=False).head(10))
# Step 7: Quickly surface top 5 deals
print(df.nlargest(5, 'revenue'))
# Step 8: Find smallest orders (potential data issues or micro-transactions)
print(df.nsmallest(5, 'revenue'))
# Step 9: Sort by rep and date to spot-check one person's history
alice_sorted = df[df['rep'] == 'Alice'].sort_values('date', ascending=False)
print(alice_sorted.head(10))
This entire workflow takes under two minutes and already answers: does the data look reasonable? Are there obvious missing values? Who are the top performers? Are there any suspicious micro-transactions?
For a more systematic approach to data quality checks before analysis, see Validating and Profiling a New Dataset with pandas: Row Counts, Distributions, and Outlier Checks Before You Analyze.
One of the satisfying things about pandas is that you can chain these methods together into readable one-liners. Instead of:
sorted_df = df.sort_values('revenue', ascending=False)
top10 = sorted_df.head(10)
print(top10)
You can write:
df.sort_values('revenue', ascending=False).head(10)
Chaining works because each method returns a DataFrame, and the next method operates on that returned DataFrame. This is the same idea as nesting functions in Excel formulas, just written left-to-right instead of inside-out.
A few useful chains:
# Top 5 by revenue, showing only rep, product, and revenue columns
df.nlargest(5, 'revenue')[['rep', 'product', 'revenue']]
# Sort by region then revenue, and sample 3 from each region
df.sort_values(['region', 'revenue'], ascending=[True, False]).groupby('region').head(3)
# Bottom 10 transactions, randomly shuffled — useful for varied spot-checking
df.nsmallest(10, 'revenue').sample(frac=1, random_state=7)
Key insight
Chaining methods is idiomatic pandas — it's how experienced practitioners write exploratory code. It keeps your intermediate results from cluttering the namespace and makes it easy to read what's happening in sequence. Just don't chain so many steps that debugging becomes painful.
Work through these tasks using the sales DataFrame we created at the start of the lesson. Try each one before reading the solution below.
Task 1: Show the first 8 rows of the DataFrame.
Task 2: Take a random sample of 20 rows with random_state=2024. How many of them have a NaN in discount_pct?
Task 3: Sort the DataFrame by region (ascending) and then by revenue (descending). Display the first 15 rows.
Task 4: Find the 5 transactions with the lowest units sold.
Task 5: For the product "Monitor" only, find the top 3 transactions by revenue.
Solutions:
# Task 1
df.head(8)
# Task 2
sample = df.sample(20, random_state=2024)
print(sample['discount_pct'].isnull().sum())
# Task 3
df.sort_values(['region', 'revenue'], ascending=[True, False]).head(15)
# Task 4
df.nsmallest(5, 'units')
# Task 5
df[df['product'] == 'Monitor'].nlargest(3, 'revenue')
"I sorted my DataFrame but the original still looks unsorted."
Right — sort_values() returns a new DataFrame. Either assign it (sorted_df = df.sort_values(...)) or use inplace=True. Prefer the assignment approach for clarity.
"I used head() after sort_values() but got wrong results."
Make sure you're chaining correctly. df.sort_values('revenue', ascending=False).head(5) works. But if you wrote df.sort_values('revenue', ascending=False) on one line and df.head(5) on the next, you're calling head() on the original df, not the sorted one.
"nlargest() gives me an error about non-numeric data."
nlargest() and nsmallest() only work on numeric columns. If your revenue column was accidentally loaded as a string (check with df.dtypes), you'll need to convert it first: df['revenue'] = pd.to_numeric(df['revenue'], errors='coerce'). See Cleaning Messy Data with pandas: Missing Values, Duplicates, and Data Types for a full guide to type conversion.
"sample() gives different results every time."
That's expected behavior. If you need reproducibility, always set random_state=<any integer>.
"I sorted by date but the order looks wrong."
If your date column is stored as a string (e.g., '2023-01-15'), alphabetical string sorting usually works for ISO-format dates (YYYY-MM-DD). But if your dates look like '01/15/2023', string sorting will fail. Convert your column to a proper datetime first: df['date'] = pd.to_datetime(df['date']). Learn more about working with date columns in Working with Dates and Time Series in pandas: Parsing, Resampling, and Rolling Windows.
Here's what you can now do:
| Method | What it does | When to use it |
|---|---|---|
head(n) |
First n rows (default 5) | First look at structure and top rows |
tail(n) |
Last n rows (default 5) | Check for trailing junk, truncation |
sample(n) |
Random n rows | Spot-check data quality throughout |
sort_values(col) |
Sort by column(s) | Full ordering, multiple columns, text |
nlargest(n, col) |
Top n rows by numeric column | Quick top-N leaderboard |
nsmallest(n, col) |
Bottom n rows by numeric column | Smallest orders, outliers |
These methods form the core of your first-look workflow every time you touch a new dataset. They're simple individually, but used together they give you a remarkably complete picture of what you're working with before you write a line of serious analysis.
Where to go next: