SUMIF and SUMIFS Equivalent in pandas
SUMIF and SUMIFS are the workhorses of a real spreadsheet, and they are the first formulas
anybody automating a report has to reproduce. The pandas equivalent is not a function but a pattern:
build a boolean mask, select rows with it, and sum the column you want. This guide, part of
Excel Formula Equivalents in pandas,
covers the one-condition case, the multi-condition case, and the grouped form that replaces a
formula copied down 50,000 rows.
Prerequisites
pip install pandas openpyxl
Every example below runs against this sample workbook:
import pandas as pd
sales = pd.DataFrame({
"Region": ["North", "South", "North", "West", "South", "North", "West"],
"Rep": ["Ana", "Ben", "Cara", "Dev", "Eve", "Ana", "Dev"],
"Product": ["Widget", "Gadget", "Widget", "Widget", "Gadget", "Gadget", "Widget"],
"Revenue": [12400.0, 9800.5, 15320.25, 7010.0, 4300.75, 6120.0, 11450.5],
"Units": [124, 98, 153, 70, 43, 61, 114],
"Ordered": pd.to_datetime([
"2026-01-04", "2026-01-19", "2026-02-02", "2026-02-27",
"2026-03-08", "2026-03-15", "2026-03-30",
]),
})
sales.to_excel("sales.xlsx", sheet_name="Detail", index=False)
SUMIF with one condition
# =SUMIF(A:A, "North", D:D)
north = sales.loc[sales["Region"] == "North", "Revenue"].sum()
print(f"{north:,.2f}")
Three things are happening, and separating them is what makes everything later easy.
sales["Region"] == "North" produces a boolean Series — one True or False per row. .loc[mask, "Revenue"] selects the Revenue values from the rows where the mask is True. .sum() totals them.
Because the mask is an ordinary object you can inspect it, which Excel does not let you do:
mask = sales["Region"] == "North"
print(f"{mask.sum()} of {len(mask)} rows match")
print(sales.loc[mask, ["Rep", "Revenue"]])
That inspection step is where translation bugs get caught. A SUMIF returning an unexpected total gives you no way to see which rows it added; a mask shows you immediately.
SUMIFS with several conditions
Conditions combine with & for AND and | for OR. The parentheses are mandatory — Python's
operator precedence binds & more tightly than ==, so omitting them produces a confusing error
about ambiguous truth values.
# =SUMIFS(D:D, A:A, "North", C:C, "Widget")
mask = (sales["Region"] == "North") & (sales["Product"] == "Widget")
print(sales.loc[mask, "Revenue"].sum())
# =SUMIFS(D:D, D:D, ">10000", A:A, "<>West")
mask = (sales["Revenue"] > 10000) & (sales["Region"] != "West")
print(sales.loc[mask, "Revenue"].sum())
Excel expresses comparisons as strings — ">10000" — because a formula argument has to be text.
pandas uses real operators, which means no quoting rules to remember and no silent failure when a
criteria string is malformed.
Dates work the same way, and a half-open interval avoids the boundary double-count that catches people summing month by month:
# =SUMIFS(D:D, F:F, ">=2026-02-01", F:F, "<2026-03-01")
february = (sales["Ordered"] >= "2026-02-01") & (sales["Ordered"] < "2026-03-01")
print(sales.loc[february, "Revenue"].sum())
The grouped form that replaces a copied-down SUMIF
A SUMIF written into every row of a 50,000-row sheet scans the whole column 50,000 times, which is why those workbooks take a minute to open. The pandas equivalent scans once and distributes the answer.
# Excel: =SUMIF(A:A, A2, D:D) filled down the sheet
sales["Region total"] = sales.groupby("Region")["Revenue"].transform("sum")
# Several keys, several aggregates — no extra passes
sales["Region product total"] = sales.groupby(["Region", "Product"])["Revenue"].transform("sum")
sales["Share of region"] = sales["Revenue"] / sales["Region total"]
print(sales[["Region", "Product", "Revenue", "Region total", "Share of region"]])
transform is the function to reach for whenever the requirement is "give every row its group's
total". The share calculation on the last line is the reason it matters: with the group total on
each row, a percentage-of-total column is one division rather than a second SUMIF.
Producing a summary table instead
When the output is a summary rather than an enriched detail sheet, groupby().agg() gives it
directly — and gives several aggregates in one pass, which SUMIFS cannot do at all.
summary = sales.groupby("Region", as_index=False).agg(
Revenue=("Revenue", "sum"),
Units=("Units", "sum"),
Orders=("Revenue", "size"),
Best=("Revenue", "max"),
)
summary["Average order"] = (summary["Revenue"] / summary["Orders"]).round(2)
print(summary.sort_values("Revenue", ascending=False))
Naming the outputs in agg is worth the extra characters: the resulting frame has flat, meaningful
column names rather than a multi-level index that then has to be flattened before it can be written
to a sheet. That single habit removes most of the friction between an aggregation and
Write a Pandas DataFrame to Excel Without the Index.
Wildcards and partial matches
Excel's criteria accept * and ?. The pandas equivalents are string methods, which are more
capable and slightly more verbose.
# =SUMIF(C:C, "Widg*", D:D)
starts = sales["Product"].str.startswith("Widg", na=False)
print(sales.loc[starts, "Revenue"].sum())
# =SUMIF(B:B, "*a*", D:D) — contains
contains = sales["Rep"].str.contains("a", case=False, na=False)
print(sales.loc[contains, "Revenue"].sum())
# Several values at once — Excel needs one SUMIF per value plus addition
chosen = sales["Region"].isin(["North", "West"])
print(sales.loc[chosen, "Revenue"].sum())
na=False is not optional in practice. Without it, a missing value in the column makes
str.contains return NaN for that row, and using a mask containing NaN raises rather than skipping
the row — a failure that only appears once real data arrives.
isin deserves particular attention because it has no clean Excel equivalent at all. A SUMIF over
five acceptable values means five formulas added together, which is exactly the kind of thing that
silently stops being maintained when a sixth value appears.
Weighted totals, and the SUMPRODUCT case
Once a report grows past simple conditional sums it usually reaches for SUMPRODUCT, which multiplies arrays element by element before totalling them. That is a natural operation on columns, so it reads better in pandas than it does in a formula bar.
# =SUMPRODUCT((A2:A100="North")*(D2:D100)*(E2:E100))
mask = sales["Region"] == "North"
weighted = (sales["Revenue"] * sales["Units"]).loc[mask].sum()
# A weighted average price, which SUMIF alone cannot express
by_region = sales.groupby("Region").apply(
lambda part: (part["Revenue"] * part["Units"]).sum() / part["Units"].sum(),
include_groups=False,
)
print(by_region.round(2))
The include_groups=False argument keeps recent pandas versions from warning about the grouping
column being passed into the function; without it the code still works but emits a deprecation
notice that clutters a scheduled job's log.
Weighted aggregates are the point where a spreadsheet usually starts to sprawl into helper columns — a product column here, a subtotal row there — and where moving the calculation into Python collapses several sheets into a handful of lines. The helper columns were only ever there because a formula could not hold two operations at once.
Reproducing a subtotal row
Reports frequently end with a total row, and a SUMIF-based sheet gets it from another formula. In pandas the equivalent is to compute the summary and append the total explicitly, which has the advantage of being visible rather than hidden in row 200.
summary = sales.groupby("Region", as_index=False)["Revenue"].sum()
total = pd.DataFrame({"Region": ["Total"], "Revenue": [summary["Revenue"].sum()]})
with_total = pd.concat([summary, total], ignore_index=True)
print(with_total)
Building the total from the summary rather than from the source frame is deliberate: it guarantees the total equals the sum of the rows above it, which is exactly the property that breaks in a spreadsheet when a SUM range stops one row short of the data. If the two ever need to disagree — because some rows are excluded from the breakdown — that becomes an explicit decision rather than a range that quietly drifted.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
ValueError: The truth value of a Series is ambiguous | and/or used instead of &/| | Use the bitwise operators and parenthesise each condition |
| Total is 0 or values are concatenated | The column is text, not numeric | pd.to_numeric(col, errors="coerce") before summing |
| Fewer rows match than in Excel | Trailing spaces or case differences in the key | .str.strip() and .str.casefold() first |
str.contains raises on missing values | NaN in the column | Pass na=False |
| Boundary dates counted twice | Two inclusive comparisons | Use a half-open interval: >= start and < end |
transform result is all NaN | Grouping on a column containing NaN | dropna=False on groupby, or fill the key first |
Performance and scale
The performance difference is not marginal. A conditional total over 200,000 rows is a single vectorised pass in pandas — milliseconds — while the same work as a copied-down SUMIF is a quadratic amount of scanning that Excel repeats on every recalculation.
Two habits keep the pandas version fast as data grows. Compute masks once and reuse them rather than
rebuilding the same comparison for each aggregate. And prefer a single groupby().agg() over
several separate masked sums when the groups are the same, because each pass over the frame costs
something even when that something is small.
# One pass, three answers
by_region = sales.groupby("Region")["Revenue"].agg(["sum", "mean", "count"])
# Rather than three passes over the same rows
north = sales.loc[sales["Region"] == "North", "Revenue"]
For genuinely large files, the read is the bottleneck long before the aggregation is — which is the argument in Read a Large Excel File in Chunks with Pandas.
Conclusion
SUMIF becomes df.loc[mask, col].sum(), SUMIFS becomes the same thing with conditions joined by
&, and a SUMIF copied down a column becomes groupby().transform("sum") — one pass instead of one
per row. Build the mask as a named variable so you can inspect what matched, coerce numeric columns
before summing, and reach for groupby().agg() when the output is a summary table rather than an
enriched detail sheet.
Frequently asked questions
How do I write SUMIFS with a date range? Build the mask with two comparisons combined by &, exactly as you would for any other pair of conditions: (df'Ordered' >= start) & (df'Ordered' < end). Using a half-open interval avoids the classic double-counting of the boundary day.
What is the difference between transform('sum') and groupby().sum()? groupby().sum() returns one row per group. transform('sum') returns one value per original row — the group's total, aligned back to every member. transform is the direct equivalent of a SUMIF copied down a column.
Why does my sum come out as 0 or as concatenated text? The column is text rather than numeric. Excel coerces silently; pandas does not. Run pd.to_numeric(col, errors='coerce') first, then check how many NaN values it produced.
How do I reproduce SUMIF's wildcard criteria? Use .str.contains() with a regular expression, or .str.startswith(). Excel's asterisk becomes .* and its question mark becomes a single-character match; remember to pass na=False so missing values do not raise.
Can I sum several columns at once? Yes — pass a list of column names to the selection: df.locmask, 'Revenue', 'Units'.sum(). Excel needs a separate SUMIFS per column, which is one of the places the translation is plainly better.
Related
- Up one level: Excel Formula Equivalents in pandas — the whole function map and the pattern behind it.
- COUNTIF and COUNTIFS Equivalent in pandas — the same masks, counted instead of summed.
- Running Totals and Year-Over-Year Growth in pandas — cumulative and period-over-period versions.
- Create a Pivot Table from Excel with Pandas — the cross-tab form of the same aggregation.
- Group Excel Rows by Month and Quarter with Pandas — date-based grouping without building the mask by hand.