Raw view: Pandas
No invisible, zero-width, or bidi-control characters detected.
---
description: This guide outlines definitive best practices for writing high-performance, maintainable, and robust pandas code, focusing on modern patterns and avoiding common pitfalls.
globs: **/*.py
---
# pandas Best Practices
Pandas is the backbone of data analysis in Python. Adhere to these guidelines to write efficient, readable, and scalable code that integrates seamlessly into modern AI/ML pipelines.
## 1. Code Organization & Structure
### 1.1 Standard Imports
Always import pandas with its conventional alias for consistency.
❌ BAD
```python
import pandas
from pandas import DataFrame
```
✅ GOOD
```python
import pandas as pd
import numpy as np # For numerical operations
```
### 1.2 Method Chaining for Readability
Chain operations to create clear, sequential data transformations. Use `.pipe()` for custom functions or when intermediate steps improve clarity.
❌ BAD
```python
df_filtered = df[df['value'] > 10]
df_grouped = df_filtered.groupby('category')
df_result = df_grouped['metric'].mean().reset_index()
```
✅ GOOD
```python
df_result = (
df[df['value'] > 10]
.groupby('category')['metric']
.mean()
.reset_index()
.rename(columns={'metric': 'avg_metric'}) # Add a descriptive rename
)
```
## 2. Common Patterns & Anti-patterns
### 2.1 Avoid Python Loops – Embrace Vectorization
Iterating over DataFrames row-by-row (`.iterrows()`, `.apply(axis=1)`) is a major performance bottleneck. Pandas operations are optimized C-level functions.
❌ BAD (Slow for large DataFrames)
```python
# Calculating a new column based on existing ones
for index, row in df.iterrows():
df.loc[index, 'new_col'] = row['col_a'] + row['col_b']
# Complex conditional logic with .apply(axis=1)
def calculate_status(row):
if row['score'] > 90 and row['grade'] == 'A':
return 'Excellent'
return 'Good'
df['status'] = df.apply(calculate_status, axis=1)
```
✅ GOOD (Vectorized and performant)
```python
# Calculating a new column
df['new_col'] = df['col_a'] + df['col_b']
# Complex conditional logic using np.select or boolean indexing
conditions = [
(df['score'] > 90) & (df['grade'] == 'A'),
(df['score'] > 80) & (df['grade'] == 'B')
]
choices = ['Excellent', 'Very Good']
df['status'] = np.select(conditions, choices, default='Good')
```
### 2.2 Use `.loc` and `.iloc` for Explicit Access
Always use `.loc` (label-based) or `.iloc` (integer-based) for setting values to prevent `SettingWithCopyWarning` and ensure predictable behavior.
❌ BAD (Chained indexing can return a view, leading to `SettingWithCopyWarning`)
```python
df[df['city'] == 'London']['population'] = 8_000_000
```
✅ GOOD (Explicitly modifies the original DataFrame)
```python
df.loc[df['city'] == 'London', 'population'] = 8_000_000
```
### 2.3 Leverage `df.eval()` and `df.query()`
For complex filtering or column creation, `eval()` and `query()` can be significantly faster than boolean indexing, as they use the underlying C engine.
❌ BAD
```python
df_filtered = df[(df['col_a'] > 10) & (df['col_b'] < 20) | (df['col_c'] == 'X')]
df['new_col'] = df['col_a'] * df['col_b'] + df['col_c'] / df['col_d']
```
✅ GOOD
```python
df_filtered = df.query("(col_a > 10 and col_b < 20) or (col_c == 'X')")
df['new_col'] = df.eval("col_a * col_b + col_c / col_d")
```
## 3. Performance Considerations
### 3.1 Optimize Data Loading with `read_csv`
Specify `dtype` and `usecols` upfront to reduce memory footprint and speed up loading, especially for large files. Let `read_csv` infer optimal defaults (`low_memory=False` is often better).
❌ BAD
```python
df = pd.read_csv("large_data.csv") # Pandas infers all dtypes, loads all columns
```
✅ GOOD
```python
# Define dtypes and select only necessary columns
dtypes = {
'store_id': 'Int32', # Use nullable integer
'product_id': 'Int32',
'category': 'category', # For repeated strings
'price': 'Float32',
'timestamp': 'datetime64[ns]'
}
cols_to_use = ['store_id', 'product_id', 'category', 'price', 'timestamp']
df = pd.read_csv(
"large_data.csv",
dtype=dtypes,
usecols=cols_to_use,
parse_dates=['timestamp'], # Parse dates explicitly
low_memory=False # Allows pandas to infer dtypes more accurately
)
```
### 3.2 Use Optimal Data Types
Explicitly set the smallest appropriate dtype. `category` for repeated strings, `Int64` (nullable integer), `StringDtype` (nullable string), and smaller floats/integers save memory and improve performance.
❌ BAD
```python
df['id_col'] = df['id_col'].astype('int64') # Default, but maybe not needed
df['product_name'] = df['product_name'].astype('object') # Default string
```
✅ GOOD
```python
df['id_col'] = df['id_col'].astype('Int32') # Use nullable, smaller int
df['product_name'] = df['product_name'].astype('StringDtype') # Modern nullable string
df['region'] = df['region'].astype('category') # For categorical data
df['sales'] = df['sales'].astype('Float32') # Smaller float if precision allows
```
## 4. Common Pitfalls & Gotchas
### 4.1 Handle Missing Values Explicitly
Always address `NaN` values. Use `.fillna()`, `.dropna()`, or `.interpolate()` with clear strategies.
❌ BAD
```python
# Operations on columns with NaNs can lead to unexpected results or errors
df['revenue_per_sale'] = df['total_revenue'] / df['num_sales'] # NaNs propagate
```
✅ GOOD
```python
# Fill NaNs before calculation or drop them
df['num_sales'] = df['num_sales'].fillna(0) # Assume 0 sales if missing
df['revenue_per_sale'] = df['total_revenue'] / df['num_sales']
# Or drop rows with NaNs in critical columns
df_cleaned = df.dropna(subset=['total_revenue', 'num_sales'])
```
### 4.2 Be Mindful of Implicit Type Conversions
Operations can silently change dtypes. Monitor with `df.info()` and explicitly cast when necessary.
❌ BAD
```python
# Adding a float to an Int64 column can convert it to float64, losing nullable int
df['int_col'] = pd.Series([1, 2, pd.NA], dtype='Int64')
df['int_col'] = df['int_col'] + 0.5 # Converts to float64
```
✅ GOOD
```python
df['int_col'] = pd.Series([1, 2, pd.NA], dtype='Int64')
# If you need float, explicitly convert or accept the float result
df['float_col'] = df['int_col'].astype('Float64') + 0.5
```
## 5. Type Hints
### 5.1 Type Your Pandas Functions
Use `pd.DataFrame`, `pd.Series`, and specific dtypes for function signatures to improve code clarity, enable static analysis, and reduce bugs.
❌ BAD
```python
def process_data(data):
# ... untyped operations ...
return data
```
✅ GOOD
```python
from typing import Dict, Any
import pandas as pd
def process_sales_data(df: pd.DataFrame) -> pd.DataFrame:
"""
Processes raw sales data to calculate aggregated metrics.
"""
df['total_price'] = df['quantity'] * df['unit_price']
return df.groupby('product_id')['total_price'].sum().reset_index()
def create_empty_dataframe(columns: Dict[str, Any]) -> pd.DataFrame:
"""Creates an empty DataFrame with specified columns and dtypes."""
return pd.DataFrame(columns=list(columns.keys())).astype(columns)
# Example usage with type hints
schema = {
'product_id': 'Int64',
'quantity': 'Int32',
'unit_price': 'Float32'
}
empty_df = create_empty_dataframe(schema)
```
## 6. Virtual Environments
### 6.1 Always Use Virtual Environments
Isolate project dependencies using `venv` or `conda`. This prevents dependency conflicts and ensures reproducible environments.
```bash
# Create a virtual environment
python -m venv .venv
# Activate it
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install pandas
pip install pandas ruff black isort pytest
```
## 7. Packaging
### 7.1 Manage Dependencies with `pyproject.toml`
For any project beyond a simple script, use `pyproject.toml` (with `poetry` or `setuptools`) to declare dependencies, including `pandas`.
```toml
# pyproject.toml
[project]
name = "my_data_pipeline"
version = "0.1.0"
dependencies = [
"pandas>=2.3.0",
"numpy",
"pyarrow", # Recommended for faster I/O and nullable dtypes
]
[tool.ruff]
line-length = 88
target-version = "py310"
```
## 8. Testing Approaches
### 8.1 Write Comprehensive Tests with `pytest`
Ensure data transformations are correct and robust. Use `pandas.testing` for DataFrame and Series comparisons.
```python
# tests/test_data_processing.py
import pandas as pd
from pandas.testing import assert_frame_equal
from my_package.data_processor import process_sales_data
def test_process_sales_data():
# Arrange
raw_data = pd.DataFrame({
'product_id': [1, 1, 2, 2],
'quantity': [10, 5, 20, 10],
'unit_price': [1.0, 2.0, 0.5, 1.5]
})
expected_data = pd.DataFrame({
'product_id': [1, 2],
'total_price': [20.0, 25.0]
})
# Act
result_df = process_sales_data(raw_data)
# Assert
assert_frame_equal(result_df, expected_data)
def test_process_sales_data_with_nans():
# Arrange
raw_data_with_nan = pd.DataFrame({
'product_id': [1, 1, 2, 2, 3],
'quantity': [10, 5, 20, 10, pd.NA],
'unit_price': [1.0, 2.0, 0.5, 1.5, 10.0]
}, dtype={'quantity': 'Int64'}) # Use nullable Int64
# Act (assuming process_sales_data handles NaNs gracefully, e.g., drops rows or fills)
# For this example, let's assume it drops rows with NA in quantity
result_df = process_sales_data(raw_data_with_nan.dropna(subset=['quantity']))
expected_data = pd.DataFrame({
'product_id': [1, 2],
'total_price': [20.0, 25.0]
})
assert_frame_equal(result_df, expected_data)
```