Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Python›Pandas — DataFrames and Series

Popular Libraries Overview

Pandas — DataFrames and Series

Pandas is a powerful Python library providing high-performance, easy-to-use data structures designed for manipulating structured or tabular data. It matters because it abstracts away complex iterative logic, allowing you to perform sophisticated data transformations with minimal, readable code. You reach for it whenever your project requires cleaning, filtering, or analyzing datasets that would be too cumbersome to manage with standard Python lists or dictionaries.

Understanding the Series

The Series is the fundamental building block of Pandas, functioning essentially as a one-dimensional, labeled array capable of holding any data type. Think of it as a hybrid between a Python list and a dictionary: it maintains a specific sequence like a list, but each element is explicitly linked to an index label, acting like a dictionary key. This index is critical because it allows for high-speed lookups and automatic alignment during operations. When you create a Series, Pandas defaults to an integer-based range index, but you can explicitly define custom labels to make your data more descriptive. The power of a Series lies in its vectorized nature; because the underlying data is organized in a contiguous memory block, operations like addition or multiplication are applied to every element simultaneously without explicit looping. Understanding this structure is essential because almost every DataFrame column is, in reality, just an individual Series objects managed together in a collection.

import pandas as pd

# Creating a Series with specific labels
prices = pd.Series([10.5, 20.0, 15.75], index=['apple', 'banana', 'cherry'])

# Accessing by label name
print(prices['banana'])

# Vectorized operation: multiplying all values at once
print(prices * 1.1)  # Applying a 10% tax increase

The DataFrame Structure

A DataFrame is a two-dimensional labeled data structure, effectively a spreadsheet in Python. You can visualize it as a collection of Series objects sharing a common index. Each column in a DataFrame represents a feature, while each row represents an observation. The internal architecture uses a dictionary-like mapping where keys are column names and values are Series objects, ensuring that all data within a single column remains of the same type for optimal performance. Because columns are aligned by their index, operations across the DataFrame remain synchronized, preventing data mismatch errors. This structure is superior to nested lists because it enforces column-based consistency and provides intuitive access to both rows and columns. When you manipulate a DataFrame, Pandas manages the metadata behind the scenes, ensuring that if you add a new column or drop a row, the labels are updated correctly. This abstraction allows you to treat a complex grid of data as a single, cohesive entity.

import pandas as pd

# Creating a DataFrame from a dictionary of lists
data = {'name': ['Alice', 'Bob'], 'age': [25, 30], 'city': ['NY', 'LA']}
df = pd.DataFrame(data)

# Accessing a column returns a Series
print(df['age'].mean())

# Adding a new derived column based on existing data
df['age_plus_one'] = df['age'] + 1
print(df)

Indexing and Slicing

Effective data retrieval is achieved through label-based and position-based indexing. The .loc attribute is used for label-based indexing, where you specify the names of rows or columns, while .iloc is used for integer-position-based indexing, similar to standard list slicing. The design choice behind separating these is to remove ambiguity; since an index label could technically be an integer, using names and positions interchangeably would lead to unpredictable outcomes. When using .loc, the slicing is inclusive of the stop value, which differs from traditional Python slicing. This approach gives you granular control over your selection, allowing you to filter based on specific criteria or extract subsets for analysis. Understanding how these accessors work ensures you can reliably extract slices without modifying the underlying dataset accidentally. Always prefer .loc for readability when your labels are known, and save .iloc for situations where you need to rely on the sequence position, such as taking the first ten rows regardless of their names.

import pandas as pd

df = pd.DataFrame({'val': [10, 20, 30, 40]}, index=['a', 'b', 'c', 'd'])

# Label-based slice (includes 'b' and 'c')
print(df.loc['b':'c'])

# Integer-based slice (includes index 0 and 1, stops before 2)
print(df.iloc[0:2])

Boolean Masking

Boolean masking is the primary method for filtering data in Pandas. By applying a conditional operator to a Series or DataFrame, you generate a new Series containing True or False values corresponding to each element. When this mask is passed back into the indexing brackets, Pandas returns only the rows or elements where the condition evaluated to True. This mechanism is incredibly efficient because it avoids explicit 'for' loops entirely, leveraging optimized C-code under the hood to perform the filtering operation. You can chain multiple conditions using bitwise operators like '&' for 'and' or '|' for 'or', provided you wrap each condition in parentheses. This approach is fundamental to data analysis, allowing you to prune datasets down to relevant subsets based on complex logic. Understanding that this creates a copy or a view of the original data is vital for memory management, as filtering large datasets often leads to the creation of new objects that consume additional system resources.

import pandas as pd

df = pd.DataFrame({'score': [85, 92, 45, 78], 'passed': [True, True, False, True]})

# Creating a boolean mask
mask = df['score'] > 80

# Applying the mask to filter the DataFrame
filtered_df = df[mask]
print(filtered_df)

Handling Missing Data

Real-world data is rarely clean; missing values are a constant challenge in data science. Pandas handles this using the 'NaN' (Not a Number) representation, which serves as a placeholder for null or invalid data points. The presence of NaN can interfere with mathematical computations, as any operation involving a null value typically results in a null result. To mitigate this, Pandas provides robust tools like .isna() to detect nulls, and .dropna() or .fillna() to manage them. Deciding whether to drop rows with missing values or fill them with a calculated mean depends on the context of your analysis; dropping might lose valuable information, while filling might introduce bias. By understanding that NaN is treated as a float in numerical columns, you can reason about how your data types might change when you introduce or replace null values. Learning to handle these gaps is essential for ensuring that your final statistical outputs are accurate and reflect the true nature of the input dataset.

import pandas as pd
import numpy as np

df = pd.DataFrame({'val': [10, np.nan, 30]})

# Detecting missing values
print(df.isna())

# Filling missing values with the mean of the column
df['val'] = df['val'].fillna(df['val'].mean())
print(df)

Key points

  • A Series is a one-dimensional array where each element has a corresponding label.
  • DataFrames represent structured data in a two-dimensional grid of rows and columns.
  • Pandas operations are vectorized, meaning they apply to entire columns at once for high performance.
  • The .loc accessor is used for label-based selection, while .iloc is used for integer-based selection.
  • Boolean masking allows for efficient filtering by creating conditional series of True and False values.
  • Missing data is typically represented by NaN and must be addressed using filling or dropping methods.
  • Indexing in Pandas is designed to be explicit to avoid the common pitfalls of ambiguous selection.
  • You should use parentheses when combining multiple conditions in boolean masks to ensure correct operator precedence.

Common mistakes

  • Mistake: Modifying a DataFrame copy without using .copy(). Why it's wrong: Modifying a slice of a DataFrame can trigger a SettingWithCopyWarning if it's a view instead of a copy. Fix: Use .copy() explicitly when creating a new object from a slice.
  • Mistake: Forgetting that Series index alignment happens automatically. Why it's wrong: Users assume operations happen by position, but Pandas aligns on index labels. Fix: Align indices before performing element-wise arithmetic between two Series.
  • Mistake: Trying to use list indexing like df[0] to access a row. Why it's wrong: Brackets on a DataFrame default to column labels, not row positions. Fix: Use .iloc[0] for position-based row access or .loc[] for label-based access.
  • Mistake: Using a loop to iterate over rows. Why it's wrong: It is extremely inefficient compared to vectorized operations. Fix: Use vectorization, apply(), or map() functions to process data in bulk.
  • Mistake: Confusing .loc and .iloc. Why it's wrong: .loc is label-based and inclusive of the stop bound, whereas .iloc is integer-based and exclusive of the stop bound. Fix: Use .loc when accessing by names and .iloc when accessing by index positions.

Interview questions

What is the fundamental difference between a Pandas Series and a DataFrame?

A Pandas Series is a one-dimensional array-like object that can hold any data type, functioning much like a single column in a spreadsheet. In contrast, a DataFrame is a two-dimensional tabular data structure with labeled axes, rows, and columns, acting like a dictionary of Series objects sharing the same index. You use a Series for single-variable analysis, while a DataFrame is essential for handling multi-dimensional datasets where rows represent observations and columns represent different features.

How do you select specific data from a DataFrame using labels versus integer positions?

To select data by label, you use the .loc accessor, which is useful when you know the index names or column headers, such as df.loc['row_label', 'column_name']. For selecting by integer position, you use the .iloc accessor, which is position-based, starting from zero. Using .iloc is safer for programmatic slicing where you do not know the index names. Knowing both is critical because .loc is inclusive of the endpoint, while .iloc follows Python's standard slicing behavior where the stop index is exclusive.

Can you explain how to handle missing data within a Pandas DataFrame?

Handling missing data typically involves methods like .isna(), .fillna(), or .dropna(). First, .isna() identifies null entries, returning a boolean mask. You then choose a strategy: if the data is sparse, .dropna() removes rows or columns with missing values. If you prefer to retain data, .fillna() replaces NaNs with a specific value, such as the mean or median of that column. This is vital for maintaining statistical integrity, as missing values can cause errors in mathematical operations if left unaddressed.

How does boolean indexing work in Pandas, and why is it efficient?

Boolean indexing allows you to filter rows based on conditional logic. You pass a condition, like df[df['age'] > 30], which creates a mask of True and False values. Pandas evaluates this mask against the DataFrame, returning only the rows where the condition is True. It is highly efficient because it leverages vectorized operations; instead of writing explicit Python for-loops to check every row, the calculation happens in optimized C code, making it significantly faster for large datasets.

Compare using .apply() versus vectorized operations when modifying column data.

Vectorized operations—like df['price'] * 0.9—perform calculations across an entire column at once using highly optimized C and Cython internals. They are the preferred approach for performance. In contrast, .apply() passes a function to every element or row, acting essentially like a loop. While .apply() is more flexible because it can handle complex, multi-step logic that cannot be vectorized, it is significantly slower. Therefore, you should always choose vectorization unless the operation is too complex to be represented in a vectorized format.

What is the purpose of the 'groupby' operation, and how does it affect the index of the resulting object?

The 'groupby' operation is a powerful tool for splitting a DataFrame into groups based on some criteria, applying a function like mean, sum, or count to each group, and then combining the results. This is essential for aggregate analysis. By default, the grouping key becomes the index of the resulting object, which can be challenging to work with. To keep the key as a regular column instead of an index, you should set 'as_index=False' within the groupby method, which makes the resulting DataFrame flatter and often easier to merge or visualize later.

All Python interview questions →

Check yourself

1. Given a DataFrame 'df', what is the result of 'df[['A', 'B']]'?

  • A.A Series containing column A and B
  • B.A DataFrame containing column A and B
  • C.A TypeError because list indexing is not supported
  • D.A list of the first two rows
Show answer

B. A DataFrame containing column A and B
Passing a list of strings to the selection operator returns a DataFrame containing those columns. The first option is wrong because a single column selection returns a Series, but multiple columns return a DataFrame. The third is wrong because list indexing is supported. The fourth is wrong because strings refer to columns, not rows.

2. How does Pandas handle missing values during an arithmetic operation like 'Series_A + Series_B'?

  • A.It treats missing values as zero
  • B.It raises a ValueError
  • C.It results in NaN at the corresponding index
  • D.It removes the rows with missing values from the result
Show answer

C. It results in NaN at the corresponding index
Pandas propagates NaN for missing values in arithmetic operations. It doesn't treat them as zero by default, nor does it raise an error or drop rows automatically. You must use fillna() if you want to treat them as zero.

3. What is the primary difference between a Series and a DataFrame?

  • A.Series is 1D and DataFrame is 2D
  • B.Series is for numbers and DataFrame is for strings
  • C.Series can have an index but DataFrames cannot
  • D.Series is faster than DataFrames
Show answer

A. Series is 1D and DataFrame is 2D
A Series is a one-dimensional array-like object, while a DataFrame is a two-dimensional, tabular data structure. Both can hold any data type and both have indices. Speed depends on the operation, not the data structure type itself.

4. What does the 'inplace=True' parameter in many Pandas methods achieve?

  • A.It makes the operation faster by skipping memory allocation
  • B.It returns a copy of the object rather than modifying it
  • C.It modifies the original object and returns None
  • D.It converts the object to a NumPy array
Show answer

C. It modifies the original object and returns None
The 'inplace=True' parameter tells Pandas to apply the operation directly to the existing object instead of returning a modified copy. It returns None because the operation is performed in memory on the original object. It does not improve speed or convert data to NumPy arrays.

5. When you perform a boolean selection like 'df[df['col'] > 5]', what is actually happening?

  • A.It converts the column to a list of integers
  • B.It returns a Series of boolean values
  • C.It filters the DataFrame to rows where the condition is True
  • D.It re-indexes the DataFrame based on the condition
Show answer

C. It filters the DataFrame to rows where the condition is True
This is boolean indexing; Pandas evaluates the condition for every row and returns a DataFrame containing only rows where the condition evaluated to True. The second option describes the result of just 'df['col'] > 5', not the whole expression.

Take the full Python quiz →

← PreviousNumPy — Arrays and OperationsNext →Requests — HTTP Calls

Python

78 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app