Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Selecting and filtering data in Pandas

Open In Colab

In the previous section, we successfully loaded the kloten_summer_2022.txt weather dataset. Let us quickly reload it to your local working directory so we have a fresh DataFrame to work with.

import pandas as pd

# Load the Kloten summer weather data, skipping metadata
data = pd.read_csv("data/kloten_summer_2022.txt", skiprows=10)

1. Selecting Columns

When performing data analysis, you rarely need every variable in your dataset. You can extract specific columns by placing the column name inside square brackets next to your DataFrame variable.

To extract a single column, pass the name as a string. This returns a one dimensional Series.

# Extract a single column
max_temps = data["MAX"]

# Prove it is a Series
print(type(max_temps))

# Look at the first 3 rows
max_temps.head(3)
<class 'pandas.Series'>
0 19.6 1 21.8 2 NaN Name: MAX, dtype: float64

To extract multiple columns, you must pass a list of column names inside the square brackets. This means you will see double brackets [[ ]].

  • The outer brackets tell Pandas you are making a selection: data[ ... ]

  • The inner brackets define the Python list of strings: ["YEARMODA", "MAX", "MIN"]

Extracting multiple columns returns a smaller DataFrame, rather than a Series.

# Extract multiple columns
subset = data[["YEARMODA", "MAX", "MIN"]]

# Prove it is a DataFrame
print(type(subset))

display(subset.head(3))
<class 'pandas.DataFrame'>
Loading...

“Note: We use the display() function here to explicitly print the DataFrame as a cleanly formatted HTML table in our notebook.”


2. Filtering Rows

Selecting columns is easy, but how do we select specific rows? In pure Python, you would use an if statement to check each row one by one. In Pandas, we use a concept called Boolean indexing to ask the entire dataset a True or False question simultaneously.

A conceptual diagram showing a source table on the left, a vertical True/False boolean mask in the middle, and the resulting filtered table on the right. Arrows illustrate that only rows corresponding to 'True' pass through the filter.

Boolean indexing conceptually acts as a filter. The mask of True/False values determines exactly which rows from the original DataFrame are allowed to pass through into your new subset.

First, we ask the question. Let us find out which days had a maximum temperature greater than 30 degrees Celsius:

# Ask the question (creates a boolean mask)
data["MAX"] > 30
0 False 1 False 2 False 3 False 4 False ... 87 False 88 False 89 False 90 False 91 False Name: MAX, Length: 92, dtype: bool

This returns a Series of True and False values, often called a mask.

To actually filter the data, we place this conditional mask inside the selection brackets. Pandas will keep every row that evaluates to True and hide every row that evaluates to False.

For absolute clarity, here is how you do it in two steps:

# Step 1: Save the mask to a variable
is_hot = data["MAX"] > 30

# Step 2: Pass the mask variable into the brackets
hot_days = data[is_hot]

However, experienced programmers usually combine this into a single line of code by placing the condition directly inside the brackets. Notice how the word data appears twice:

# The standard one-line approach
hot_days = data[data["MAX"] > 30]

display(hot_days.head(3))
Loading...

Notice the Index on the far left. It jumped from 15 to 18 to 19, preserving the original row labels of the days that met our criteria.


3. Multiple Conditions

Often, you need to filter data using multiple criteria simultaneously. You can combine logical conditions using special operators. Standard Python keywords and and or will not work here, as Pandas needs bitwise operators that perform vectorized logical comparisons on entire Series at once.

Pandas uses these operators:

  • & (AND): Both conditions must be True.

  • | (OR): At least one condition must be True.

AND Condition Example

Let us find “extreme” summer days that were both very hot (MAX > 30) but also had surprisingly cool mornings (MIN < 14).

# Using & (AND) for logical conditions
extreme_days = data[(data["MAX"] > 30) & (data["MIN"] < 14)]
display(extreme_days)
Loading...

OR Condition Example

Next, let us find days that were either extremely hot (MAX > 33) OR had very cold mornings (MIN < 10). This query will include days meeting either criteria, or both.

# Using | (OR) logic for extreme temperatures
either_extreme_days = data[(data["MAX"] > 33) | (data["MIN"] < 10)]
display(either_extreme_days)
Loading...

4. Sorting Data (sort_values)

Once you have isolated your data, you frequently want to order it. The .sort_values() method allows you to sort your DataFrame by one or more columns.

By default, Pandas sorts in ascending order (smallest to largest). To find the absolute hottest days of the summer at the top of our table, we need to set the ascending parameter to False.

# Sort by MAX temperature, descending
sorted_data = data.sort_values(by="MAX", ascending=False)
display(sorted_data.head(3))
Loading...

5. Copy-on-Write: Why Subsets Behave Safely

When you filter a DataFrame, pandas tries to be efficient with your computer’s memory. In older versions of pandas, this created a confusing situation: sometimes a subset behaved like a view into the original data, and sometimes it behaved like a fully independent copy.

Starting with pandas 3.0, pandas uses Copy-on-Write by default. This means that a subset behaves like an independent object when you modify it. Behind the scenes, pandas may still share memory to save resources, but if you write to the subset, pandas protects the original DataFrame by copying the data when needed.

This makes pandas more predictable than older versions. You can create a filtered subset and add a new column to that subset without worrying that the original DataFrame will silently change.

Let us filter the data for early June and add a new column:

# Create a subset for early June
early_june = data[data["YEARMODA"] < 20220610]

# Add a new column to the subset
early_june["IS_HOT"] = early_june["MAX"] > 25

display(early_june.head())
Loading...

The new column is added to early_june. The original data DataFrame remains unchanged.

# The new column exists in the subset
print("Columns in early_june:")
print(early_june.columns)

print("\nColumns in original data:")
print(data.columns)
Columns in early_june:
Index(['YEARMODA', 'MAX', 'MIN', 'TEMP1', 'TEMP2', 'IS_HOT'], dtype='str')

Columns in original data:
Index(['YEARMODA', 'MAX', 'MIN', 'TEMP1', 'TEMP2'], dtype='str')

Do We Still Need .copy()?

You can still use .copy() when you want to make your intention extra clear:

early_june_copy = data[data["YEARMODA"] < 20220610].copy()
early_june_copy["IS_HOT"] = early_june_copy["MAX"] > 25

display(early_june_copy.head())
Loading...

This is not wrong. It tells readers of your code: “I want this to be an independent object.”

However, in pandas 3.0, .copy() is no longer needed just to avoid the old SettingWithCopyWarning. The old warning existed because pandas could not always clearly tell whether you were modifying a view or a copy. Copy-on-Write removes this ambiguity for everyday use.

The Important Question: What Are You Trying to Change?

When writing pandas code, the most important question is not simply “Do I need a copy?” Instead, ask:

If you create a new object, use a new variable name:

hot_days = data[data["MAX"] > 30]

If you want to continue working with this subset, you can modify it:

hot_days["VERY_HOT"] = hot_days["MAX"] > 33

display(hot_days.head())
Loading...

This changes hot_days, not the original data.

If your actual goal is to change the original data DataFrame, we need a more precise selection tool. That tool is introduced in the next section: .loc[].

Concept Check: Separate object or original data?

Imagine you are writing a script to analyze weather patterns.

Scenario A: You filter the dataset to find all days where the maximum temperature exceeded 30°C, just so you can inspect them.

Scenario B: You filter the dataset to create a separate hot_days table and add a new column only to that table.

Scenario C: You want to add a new column to the original data DataFrame marking which days were hot.

Which scenario requires a more precise assignment method introduced in the next section?

A) Scenario A

B) Scenario B

C) Scenario C


6. Index based Selection (loc vs iloc)

In the previous section, we saw that filtered subsets can safely be used as separate working objects. But sometimes you do not want to create a separate object. Sometimes you want to select exact rows and columns in the original DataFrame and modify them directly.

For this, pandas provides two powerful indexers: .loc[] and .iloc[].

  • .loc[] (Label-based): Selects data based on its exact label (the name of the index row or the name of the column).

  • .iloc[] (Integer position-based): Selects data based on its strict numerical position (0, 1, 2...), exactly like a standard Python list.

A conceptual diagram showing a Pandas DataFrame with a sorted, non-sequential index. An arrow for .loc[0] points to the specific row labeled '0', while an arrow for .iloc[0] points to the physical top row of the table.

Understanding the difference between label-based (.loc) and position-based (.iloc) indexing. When data is sorted, the physical position (0) no longer matches the original row label (0).

Let us look at the difference. Imagine we sort our data so the hottest day (originally Index 18) is at the very top of the table.

# 1. .iloc looks for the row currently sitting in the very first position (position 0)
print("Result of .iloc[0]:")
print(sorted_data.iloc[0])

print("\n-------------------\n")

# 2. .loc looks for the row literally labeled '0' (which is now deep in the middle of the sorted data)
print("Result of .loc[0]:")
print(sorted_data.loc[0])
Result of .iloc[0]:
YEARMODA    20220619.0
MAX               35.1
MIN               17.1
TEMP1             30.0
TEMP2             34.8
Name: 18, dtype: float64

-------------------

Result of .loc[0]:
YEARMODA    20220601.0
MAX               19.6
MIN               11.1
TEMP1             19.5
TEMP2             15.6
Name: 0, dtype: float64

Notice how .iloc[0] returned the data for the hottest day (labeled 18), while .loc[0] hunted down the specific row labeled 0 (June 1st).

Selecting Rows AND Columns

The true power of these indexers is that you can select rows and columns at the same time by separating them with a comma: [rows, columns].

# Use .loc to get rows labeled 0 through 3, and specifically the "MAX" and "MIN" columns
subset_loc = data.loc[0:3, ["MAX", "MIN"]]
display(subset_loc)
Loading...

Output of .loc[0:3, [“MAX”, “MIN”]]

MAXMIN
019.611.1
121.812.3
2NaN12.7
327.412.8

7. Exercise: Isolate the Target Data

Let us bring all these skills together using the global cities dataset you downloaded in the previous section.

Imagine you are doing an analysis focused solely on major urban centers in Japan. You need to load the data, extract exactly what you need, and secure it in memory.

Tasks:

  1. Load worldcities.csv into a DataFrame.

  2. Filter the data to include only rows where the country column is exactly "Japan".

  3. Crucial: Make sure to append .copy() to create an independent dataset!

  4. Sort this new Japan DataFrame by the population column in descending order (largest to smallest).

  5. Display the top 5 rows, but only show the city and population columns.

(Hint for Step 5: Remember that selecting multiple columns requires a list inside the selection brackets, which looks like double brackets [[ ]]).

# Write your code here

8. Summary: Navigating the 2D Grid

In this section, you learned how to slice and dice your data to extract exactly the information you need. You now have the tools to surgically navigate large datasets without relying on manual for loops.

Key takeaways

  • Columns: Extract a single Series using df["col"] or a smaller DataFrame using a list df[["col1", "col2"]].

  • Rows: Use Boolean Indexing (df[df["col"] == value]) to act as a filter, keeping only rows that meet logical conditions.

  • Conditions: Combine multiple filters using & (AND) or | (OR), always wrapping each individual condition in parentheses ().

  • Sorting: Use .sort_values(by="col", ascending=False) to order your DataFrame.

  • Safety: With pandas 3.0, filtered subsets behave safely through Copy-on-Write.

  • Index Selection: Use .iloc[] to select rows based on their strict numerical position, and .loc[] to select based on exact row/column labels.

What comes next?

Now that you can navigate, slice, and filter your data, you might notice a glaring issue: real-world data is rarely perfect.

If you try to do math on a column where numbers are accidentally stored as text, or if a sensor went offline and left blank gaps in your dataset, your code will crash. In the next section, Cleaning the Mess, we will learn how to standardize messy column headers, fix text strings, and handle the infamous NaN (Not a Number) so your data is pristine and ready for analysis!