Group By to Pivot Table – Converting GroupBy Results to Pivot Tables in Pandas 2026

Data Manipulation Mar 18, 2026

Group By to Pivot Table – Converting GroupBy Results to Pivot Tables in Pandas 2026

Many data manipulation tasks start with a groupby() and then need to be reshaped into a pivot table format for reporting or visualization. In 2026, understanding how to smoothly transition from GroupBy aggregations to beautiful pivot tables is a highly valuable skill.

TL;DR — Two Main Approaches

  • Do aggregation with groupby().agg() → then .unstack()
  • Use pivot_table() directly (often cleaner and more flexible)

1. Starting with GroupBy and Converting to Pivot

import pandas as pd

# Create a small demo file so this example runs as-is
# (in your project, sales_data.csv is your real export)
import numpy as np
rng = np.random.default_rng(42)
n = 60
pd.DataFrame({
    "order_date": pd.date_range("2025-10-01", periods=n, freq="3D"),
    "customer_id": rng.integers(101, 121, n),
    "customer_name": rng.choice([" ana", "ben ", "chen", "dana"], n),
    "product_id": rng.integers(1, 9, n),
    "region": rng.choice(["North", "South", "East", "West"], n),
    "category": rng.choice(["Electronics", "Books", "Home"], n),
    "payment_method": rng.choice(["card", "cash", "wallet"], n),
    "segment": rng.choice(["consumer", "business"], n),
    "quantity": rng.integers(1, 6, n),
    "amount": rng.uniform(50, 2500, n).round(2),
}).assign(
    ship_date=lambda d: d["order_date"] + pd.Timedelta(days=2),
    status=lambda d: np.where(d["amount"] > 2000, "priority", "standard"),
).to_csv("sales_data.csv", index=False)

df = pd.read_csv("sales_data.csv", parse_dates=["order_date"])

# Helper columns: pivot_table() works best with real column names
df["month"] = df["order_date"].dt.to_period("M")
df["year"] = df["order_date"].dt.year

# Step 1: GroupBy aggregation
grouped = (
    df
    .groupby(["region", "month"])
    .agg(total_sales=("amount", "sum"))
    .reset_index()
)

# Step 2: Convert to Pivot Table format
pivot = grouped.pivot(
    index="region",
    columns="month",
    values="total_sales"
).round(2)

print(pivot)

2. Cleaner Approach: Direct pivot_table() (Recommended)

# Much cleaner and more powerful
pivot = pd.pivot_table(
    df,
    values="amount",
    index="region",
    columns="month",
    aggfunc="sum",
    margins=True,
    margins_name="Total"
).round(2)

print(pivot)

3. Advanced Example: Multiple Metrics in Pivot Form

report = pd.pivot_table(
    df,
    values=["amount", "quantity"],
    index=["region", "category"],
    columns="month",
    aggfunc={
        "amount": ["sum", "mean"],
        "quantity": "sum"
    },
    # no margins here: margins=True fails with a dict of function lists in pandas 3.1
).round(2)

print(report)

4. Best Practices in 2026

  • Use pivot_table() directly when possible — it is usually cleaner than groupby + unstack
  • Use groupby().agg() + .unstack() only when you need very custom logic before pivoting
  • Extract date components with .dt.to_period() for clean month/year columns
  • Add margins=True to get grand totals automatically
  • Use named aggregation inside pivot_table() when you need multiple metrics

GroupBy and Pivot Tables Are Two Views of One Result

df.groupby(["a", "b"])["x"].sum().unstack() and df.pivot_table(index="a", columns="b", values="x", aggfunc="sum") produce the same grid. The groupby version is flexible and reads well in a method chain; the pivot version states the final layout in one call and adds conveniences such as fill_value and margins. Knowing both lets you switch freely: start with groupby while exploring, then move to pivot_table() for the final report.

Worked Example: Proving the Two Are Equal

This example builds the same table both ways and checks that they match.

import pandas as pd

df = pd.DataFrame({
    "region": ["East", "East", "West", "West", "West"],
    "channel": ["web", "shop", "web", "web", "shop"],
    "amount": [10, 20, 30, 40, 50],
})

via_groupby = df.groupby(["region", "channel"])["amount"].sum().unstack(fill_value=0)
via_pivot = df.pivot_table(index="region", columns="channel", values="amount",
                           aggfunc="sum", fill_value=0)
print(via_pivot)
print(via_groupby.equals(via_pivot))  # True

Both grids show East with 20 in shop and 10 in web, and West with 50 and 70. When the check prints False, compare dtypes first; a missing combination can turn integers into floats in one version but not the other.

Common Mistakes

  • Forgetting unstack(): a two-key groupby returns a long Series, not a grid
  • Unstacking the wrong level: pass the level name, for example unstack("channel")
  • Different fill rules: use the same fill_value in both approaches
  • Expecting a pivot to reshape without aggregating: use pivot() when each pair appears only once

Quick Summary

  • groupby + unstack equals pivot_table
  • Explore with groupby, report with pivot_table
  • Compare results with equals() and check dtypes

Conclusion

Converting GroupBy results into pivot table format is a very common workflow in data manipulation. In 2026, the recommended approach is to use pd.pivot_table() directly whenever possible because it is more concise, flexible, and readable than doing a groupby followed by unstack. Mastering both techniques gives you full control over how you reshape and present your data.

Next steps:

  • Take a GroupBy summary you currently use and convert it into a clean pivot table using pivot_table()

Related Tutorials

Share:
Last updated: March 2026
Built-in Functions in Python 2026 str() in Python 2026: String Conversion + Modern Formatting & Best Practices memoryview with JAX in Python 2026: Zero-Copy NumPy → JAX Array Interop + Efficient ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + GPU Pinning & ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + ML Examples memoryview with NumPy & PyTorch in Python 2026: Zero-Copy Views, Efficient Slicing & ML Interop Examples memoryview with NumPy in Python 2026: Zero-Copy Views, Efficient Slicing & Real ML Examples memoryview() in Python 2026: Zero-Copy Magic for Large Binary Data + Real Examples import() in Python 2026: How It Works, Security Risks & Modern Alternatives (importlib) zip() in Python 2026: How to Use It, strict=True Behavior & Real-World Examples vars() in Python 2026: Accessing Object Namespace + Modern Introspection Patterns type() in Python 2026: Dynamic Type Inspection & Object Creation + Modern Patterns tuple() in Python 2026: Immutable Sequences + Modern Patterns & Best Practices super() in Python 2026: Method Resolution & Modern Inheritance Patterns sum() in Python 2026: Summing Iterables + Modern Numeric Patterns & Best Practices staticmethod() in Python 2026: Static Methods, Utility Functions & Modern Best Practices sorted() in Python 2026: Sorting Iterables + Modern Patterns & Best Practices slice() in Python 2026: Creating Slice Objects + Modern Patterns & Best Practices setattr() in Python 2026: Dynamic Attribute Setting + Modern Patterns & Safety set() in Python 2026: Mutable Sets Creation + Modern Patterns & Best Practices