Skip to content
ExponaBLOG
PRACTICAL AI & MACHINE LEARNING · DAY 004 / 100

pandas for data preparation: what developers should know

Load, clean and explore tabular data.

Expona Admin8 min read

Why pandas matters for machine learning

Before your data ever reaches a model, it lives in tables. Most real production data arrives as CSV exports, database queries, or Parquet files—not clean, numeric arrays ready for training. Pandas is the bridge between raw tabular data and machine learning pipelines, and mastering pandas for data preparation: what developers should know separates engineers who ship ML systems from those who ship broken ones.

This guide walks you through the core operations we use every day: loading data from multiple sources, inspecting what you're actually working with, cleaning the mess, and building features that models can learn from. We'll focus on patterns that work in production, where datasets are messy and your time is valuable.

Loading data: choosing the right format and method

You'll encounter three main ways to ingest tabular data: CSV files, Parquet, and direct database queries. Each has trade-offs in speed, storage, and ease of use.

CSV is everywhere. It's human-readable, easy to share, and works on any platform. The downside: pandas has to parse text, which slows down large files and wastes disk space.

python
import pandas as pd

# Basic CSV load
df = pd.read_csv('orders.csv')

# For large files, specify dtypes to speed up parsing
dtypes = {
    'order_id': 'int64',
    'user_id': 'int64',
    'amount': 'float32',
    'status': 'category'
}
df = pd.read_csv('orders.csv', dtype=dtypes)

Parquet is a columnar binary format. It's faster to read, compresses better, and preserves types automatically. If you're processing the same dataset repeatedly, convert to Parquet once.

python
# Read Parquet (much faster for large files)
df = pd.read_parquet('orders.parquet')

# Write to Parquet for future use
df.to_parquet('orders.parquet', compression='snappy')

Databases let you query only what you need. This is crucial when your full dataset is 100 GB but you only need a training slice.

python
import sqlalchemy

engine = sqlalchemy.create_engine('postgresql://user:pass@localhost/mydb')

# Query directly into a DataFrame
query = """
SELECT order_id, user_id, amount, created_at, status
FROM orders
WHERE created_at >= '2024-01-01'
"""
df = pd.read_sql(query, engine)

In production, we often chain these: pull from the database, convert to Parquet for caching, then reload the Parquet file for analysis. This minimizes database load and keeps your pipeline fast across multiple runs.

Inspect first: understand your data before touching it

The biggest mistake developers make is jumping straight to cleaning. You can't fix data you don't understand. Before writing any transformation, run these three commands.

df.info() shows you the shape, column names, types, and null counts. This is your first reality check.

python
df.info()

# Output:
# RangeIndex: 10000 entries, 0 to 9999
# Data columns (total 5 columns):
#  #   Column      Non-Null Count  Dtype
# ---  ------      --------------  -----
#  0   order_id    10000 non-null  int64
#  1   user_id     9950 non-null   int64
#  2   amount      10000 non-null  float64
#  3   created_at  10000 non-null  object
#  4   status      10000 non-null  object
# dtypes: int64(2), float64(1), object(2)

Notice that user_id has only 9,950 non-null values out of 10,000. That's a red flag. Also, created_at is object (string), not datetime—we'll fix that later.

df.describe() gives you statistics: min, max, mean, quartiles. It catches outliers and impossible values.

python
df.describe()

#            order_id       user_id        amount
# count    10000.000000  9950.000000  10000.000000
# mean     5000.500000  5010.230000    156.782000
# std      2886.896543  2891.456543     98.456000
# min        1.000000     1.000000      0.010000
# 25%     2500.750000  2505.573000     78.450000
# 50%     5000.500000  5010.230000    156.890000
# 75%     7500.250000  7515.887000    235.120000
# max    10000.000000 10000.000000   1250.000000

The amount column ranges from 0.01 to 1250. Is that realistic? If your orders should be between $5 and $500, those outliers matter.

df.isna().sum() breaks down null counts by column, giving you a clear picture of data quality.

python
df.isna().sum()

# order_id        0
# user_id        50
# amount          0
# created_at      0
# status          0
# dtype: int64

Now you know exactly what you're dealing with: 50 missing user IDs, everything else complete. You can decide: drop those rows, fill them with a default, or investigate why they're missing.

Clean: fix types, handle missing values, remove duplicates

Data cleaning is where most ML projects spend their time. A few common operations handle 80% of real-world mess.

Fix types. Dates almost always arrive as strings. Convert them early, before any filtering or aggregation.

python
# Convert strings to datetime
df["created_at"] = pd.to_datetime(df["created_at"])

# Now you can do time-based operations
df = df[df["created_at"] >= "2024-01-01"]
df["day_of_week"] = df["created_at"].dt.day_name()

# Convert low-cardinality columns to category
df["status"] = df["status"].astype("category")

Handle missing values. You have a few options: drop rows, forward-fill for time series, or fill with a default. Choose based on why the data is missing.

python
# Drop rows where critical columns are null
df = df.dropna(subset=["order_id", "user_id", "amount"])

# For user_id, fill with a special value (e.g., -1 for "unknown")
df["user_id"].fillna(-1, inplace=True)

# For time series, forward-fill (carry last known value forward)
df["price"].fillna(method="ffill", inplace=True)

Remove duplicates. In production systems, the same transaction can arrive twice—from retries, batch replays, or sync errors. Check your unique identifier.

python
# Keep first occurrence of each order
df = df.drop_duplicates(subset="order_id", keep="first")

# Or keep the most recent
df = df.sort_values("created_at").drop_duplicates(subset="order_id", keep="last")

Here's a complete cleaning workflow for a typical e-commerce dataset:

python
# Load raw data
df = pd.read_csv("orders.csv")

# Step 1: Inspect
print(f"Shape: {df.shape}")
print(df.info())
print(df.describe())

# Step 2: Type conversion
df["created_at"] = pd.to_datetime(df["created_at"])
df["status"] = df["status"].astype("category")

# Step 3: Remove duplicates (before handling nulls)
df = df.drop_duplicates(subset="order_id", keep="first")

# Step 4: Handle nulls
df = df.dropna(subset=["order_id", "user_id", "amount"])
df["region"].fillna("unknown", inplace=True)

# Step 5: Filter impossible values
df = df[df["amount"] > 0]
df = df[df["created_at"] >= "2023-01-01"]

print(f"Final shape: {df.shape}")

Group and aggregate: building features from raw rows

A raw transaction table is useful, but models learn from aggregated features. GroupBy + agg turns rows into insights.

Say you're building a fraud detector. You don't feed raw transactions to the model; you build features like "orders per user per day" or "average transaction amount."

python
# Count orders per user
user_order_counts = df.groupby("user_id")["order_id"].count().reset_index()
user_order_counts.columns = ["user_id", "order_count"]

# Average order amount per user
user_avg_amount = df.groupby("user_id")["amount"].mean().reset_index()
user_avg_amount.columns = ["user_id", "avg_order_amount"]

# Merge features back onto original data
df = df.merge(user_order_counts, on="user_id", how="left")
df = df.merge(user_avg_amount, on="user_id", how="left")

Use agg() to compute multiple statistics at once:

python
# Multiple aggregations in one step
user_features = df.groupby("user_id").agg({
    "order_id": "count",           # Count of orders
    "amount": ["sum", "mean", "std"],  # Total, average, volatility
    "created_at": "max"            # Most recent order
}).reset_index()

# Flatten multi-level columns
user_features.columns = [
    "user_id", "order_count", "total_amount", 
    "avg_amount", "std_amount", "last_order_date"
]

For time-based features (common in production), use groupby with a time window:

python
# Orders in the last 30 days
thirty_days_ago = pd.Timestamp.now() - pd.Timedelta(days=30)
df_recent = df[df["created_at"] >= thirty_days_ago]

recent_orders = df_recent.groupby("user_id")["order_id"].count().reset_index()
recent_orders.columns = ["user_id", "orders_last_30d"]

Common mistakes

  • Inspecting after cleaning: You've already lost visibility into what was broken. Always run df.info() and df.describe() on raw data first.
  • Ignoring data types: Dates as strings, IDs as floats, and categories as strings all break downstream operations. Convert types explicitly.
  • Handling nulls without understanding them: Don't blindly drop or fill. Ask: why are these values missing? Are they random or systematic?
  • Forgetting to reset_index() after groupby: GroupBy returns a Series or DataFrame with the grouping column as the index. You'll lose it if you don't reset.
  • Building features on the full dataset: If you're building time-based features (like "orders in the last 30 days"), ensure your time window matches your train/test split.

Key takeaways

  • Load data with the right tool: CSV for small files, Parquet for repeated reads, SQL for selective queries.
  • Always inspect with df.info(), df.describe(), and df.isna().sum() before writing any transformation.
  • Convert datetime and categorical columns explicitly; treat type fixing as a separate, documented step.
  • Remove duplicates by your unique identifier, not all columns; order matters when keeping the "first" or "last."
  • GroupBy + agg is your feature factory; most ML models learn from aggregated statistics, not raw rows.
  • In production, data cleaning is 60-70% of the work. Budget time and test your pipeline on fresh data regularly.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles