groupby Is Just SQL GROUP BY
If you know SQL, 'df.groupby('Region')['Sales'].sum()' is '
SELECT Region, SUM(Sales)
FROM dfGROUP BY Region'. The mental model carries over directly. pandas splits the rows into groups by the column you name, runs an aggregate on each group, then stitches the answers back together.
The win over Excel is that the grouping is code, not a click you redo by hand. Change the column, rerun, and every downstream total updates. No broken subtotal row, no forgotten filter.
Step 1: Group by One Column
Run 'df = pd.read_csv("orders.csv")' with columns Region, Product, Sales, and Orders. Confirm with 'df.head()' that Sales is numeric before you aggregate.
Run 'df.groupby('Region')['Sales'].sum()'. You get one total per region, for example West 4,210,000 and East 3,980,500. This is the most common groupby you'll write.
Run 'df.groupby('Region').size()'. This returns the row count per region, like SQL's COUNT(*). Use it to check a region isn't suspiciously empty before you trust its sum.
Step 2: Aggregate Several Columns at Once
Run 'df.groupby('Region').agg({'Sales':'sum', 'Orders':'mean'})'. One call returns both total Sales and average Orders per region. In SQL this is 'SELECT Region, SUM(Sales), AVG(Orders) ... GROUP BY Region'.
Run 'df.groupby('Region').agg(total_sales=('Sales','sum'), avg_orders=('Orders','mean'))'. Naming keeps your report columns clean instead of a default 'Sales' header you'd rename anyway.
Run 'df.groupby('Product')['Sales'].agg(['min','max'])'. This shows the smallest and largest Sale per product, handy for spotting outliers a single average hides.
I recommend calling '.agg()' with named tuples the first time you build a report column. The names travel straight into your exported CSV, so the file your manager opens already has headers like total_sales instead of a bare column to rename.
Step 3: Group by Two Columns
Run 'df.groupby(['Region','Product'])['Sales'].sum()'. This is a two-level GROUP BY, like 'GROUP BY Region, Product'. You get a total for every Region-and-Product pair.
Run 'df.groupby(['Region','Product'])['Sales'].sum().reset_index()'. The '.reset_index()' turns the grouped index back into normal columns, which is what you want before writing to CSV or charting.
Run 'result.sort_values('Sales', ascending=False)'. This ranks your Region-Product pairs from biggest to smallest, the exact order a top-products slide needs.
Step 4: Build a Pivot Like SQL PIVOT
Run 'df.pivot_table(index='Product', columns='Region', values='Sales', aggfunc='sum')'. This spreads Region across the top as columns and Product down the side, the pandas version of a SQL pivot or an Excel pivot table.
Run 'pivot.fillna(0)'. A product sold only in the West shows NaN in the East column; filling with 0 keeps your totals and charts from breaking on a missing value.
Run 'pivot.reset_index()' and 'pivot.to_csv("region_pivot.csv")'. You now have a wide table a non-technical reader recognizes from Excel, exported with one line.
A common mistake is calling pivot_table without aggfunc and wondering why you get a count instead of a sum. Always set aggfunc='sum' (or 'mean') explicitly, or pandas defaults to a behavior that surprises you on duplicates.
Step 5: Filter Groups, Not Just Rows
Run 'g = df.groupby('Region')' then 'big = g.filter(lambda x: len(x) > 1000)'. This drops any region with fewer than 1,000 rows, so a thin region can't distort your averages.
Run 'df.groupby('Region').apply(lambda x: x.loc[x['Sales'].idxmax()])'. This returns the single highest-Sales row inside each region, the kind of detail a regional manager asks for by name.
Run 'df.groupby('Region')['Sales'].sum().transform(lambda s: s/s.sum()*100)'. This turns each region's total into its percentage of all Sales, so West 4,210,000 might show as 38.2 percent of the company.

Worked Example: A 38 Percent West Share
Take 50,000 Orders rows with Region and Sales. 'df.groupby('Region')['Sales'].sum()' returns West 4,210,000, East 3,980,500, North 2,640,000, South 1,170,000. The company total is 12,000,500. Dividing West by the total gives 35.08 percent, not the 38 I guessed in a meeting once. The code keeps you honest; the gut guess embarrasses you.
Run 'shares = df.groupby('Region')['Sales'].sum(); print((shares/shares.sum()*100).round(1))'. You get clean one-decimal percentages ready for a slide.
Run 'shares.reset_index().to_csv("region_sales.csv", index=False)'. The file opens in Excel with Region and Sales columns, no pandas leftover to clean.
When to Group in pandas vs SQL
If the data lives in a warehouse, group there with SQL GROUP BY and HAVING and pull a small summary into Python. If the data is already a CSV on your laptop, groupby in pandas is faster than standing up a database. Pick by where the data sits, not by preference.
Step 6: Group Then Plot in One View
A grouped table is useful, but a grouped chart gets the point across in a meeting. After you total Sales per Region, pass that result straight into a bar chart so the West lead is visible without anyone reading numbers. Keeping the group and the plot in the same cell makes the report self-contained.
Run 'region_sales = df.groupby("Region")["Sales"].sum(); region_sales.plot(kind="bar")'. The bar heights are the groupby totals, so you never copy numbers by hand from the table into the chart.
Run 'plt.ylabel("Total Sales")' and 'plt.title("Sales by Region")' before 'plt.show()'. A chart with no labels forces the reader to guess what the bars mean, which defeats the purpose of plotting it.
Step 7: Group by Month for a Trend
Region totals are a snapshot; a monthly trend shows direction. Convert OrderDate to a real date, then resample by month and sum Sales. This turns a flat total into a line that answers whether you are growing without a separate pivot in Excel.
Run 'df["OrderDate"] = pd.to_datetime(df["OrderDate"]); monthly = df.set_index("OrderDate").resample("M")["Sales"].sum()'. You get one total per month, twelve points for a year of data.
Run 'df["Region"].isna().sum()' before grouping. Blank regions are dropped silently and shrink your totals with no warning, so knowing the count up front explains any shortfall later.
Run 'monthly.plot(kind="line", marker="o")'. A flat line means steady sales; a downward slope is the early warning you want before the quarter closes. I check this chart before writing any 'we are up' message.
You can group by two time levels too: 'df.set_index("OrderDate").resample("M")["Sales"].agg(["sum","count"])'. This returns both revenue and order count per month. When revenue rises but count falls, you sold fewer, bigger deals, a different story than broad growth, and one the single sum would hide.
groupby drops rows with a missing group key by default. If Region is blank on some rows, those sales vanish from every total and your grand sum comes up short with no error. Run 'df["Region"].isna().sum()' before grouping so you know how many rows the aggregate silently skipped.
How to Read the Grouped Result
A groupby result is its own small table, and reading it wrong is easy. After 'df.groupby("Region")["Sales"].sum()' you get an index of regions and a column of totals. Treat that index like a column when you export: call 'reset_index()' or your CSV opens with the region names stuck in the row labels and a blank first header that breaks the next tool.
Check the group count before you trust the sum. Run 'df.groupby("Region").size()' and confirm each region has a sensible number of rows. A region with two rows next to one with twenty thousand usually means a typo in the region name, like 'Wst' instead of 'West', and that typo splits your total into a ghost region that hides in the middle of the output.
Percentages beat raw totals when you present. A manager remembers 'West is 35 percent of sales' faster than 'West is 4,210,000'. Divide the group sums by their total and round to one decimal; the relative view also shows when a small region is growing faster than a big one, a shift the absolute numbers hide until you plot it.
Multi-column groups need a clear name. When you group by Region and Product, the result has two index levels, and a flat 'reset_index()' gives you both back as normal columns. Keep both in the export so the reader sees which product drove which region, instead of a single summed line that buries the detail a regional manager actually wanted.
Sort the result to make it useful. A grouped table sorted by the total, largest first, reads as a ranking without extra work. Chain '.sort_values("Sales", ascending=False)' onto the groupby before you export, and the file your manager opens already leads with the biggest region instead of an alphabetical order nobody asked for.
Next Steps
Grouping gives you tables; charts make them land. The next move is turning that Region total into a bar chart, which the visualization guide below covers with matplotlib and seaborn.
Write one groupby with named aggregates, reset_index, and export to CSV. That file is a finished monthly region report with no manual subtotals.
Pass the grouped result into the matplotlib guide linked below to plot Region totals as a bar chart your manager can read in two seconds.


