Quick answer: select the binary indicator columns, normalize them to pandas’ nullable Boolean dtype, then use sum() for true counts and mean() for true rates. Always report the valid-row denominator and missing-value count so a percentage cannot hide incomplete data.
To create a summary table from multiple binary columns in pandas, treat each column as a validated indicator rather than a generic number. Binary columns appear in surveys, feature matrices, event flags, quality checks, and one-hot encoded datasets. A useful summary should answer four questions for every indicator: how many rows are true, how many are false, how many are missing, and what percentage of valid rows are true.
This tutorial builds that table without row-by-row loops. If your source begins as an array, first see how to convert a NumPy array to a pandas DataFrame. For position-based inspection and slicing, Python Pool’s pandas iloc guide covers the relevant indexer.
A useful binary-column summary reports counts, rates, valid denominators, and missing values together.
Create a Binary Example DataFrame
Use one row per observation and one column per indicator. The example deliberately includes a missing value so the denominator decision is visible.
import pandas as pd
df = pd.DataFrame(
{
"segment": ["free", "free", "paid", "paid", "paid"],
"opened_email": [1, 0, 1, 1, None],
"used_search": [True, False, True, False, True],
"completed_profile": [1, 1, 0, 1, 1],
}
)
indicator_cols = [
"opened_email",
"used_search",
"completed_profile",
]
Do not select every numeric column automatically. IDs, ages, prices, and counts are numeric but not binary indicators. Keep an explicit list or enforce a naming convention such as an is_ or has_ prefix.
Normalize 0/1 Columns to Boolean
Pandas’ nullable boolean dtype can represent True, False, and pd.NA. Normalization gives the summary one consistent semantic type.
indicators = df[indicator_cols].astype("boolean")
print(indicators.dtypes)
print(indicators)
Conversion should happen only after validation. Unexpected values such as 2, "yes", or -1 may represent a data contract failure rather than truthy values that should silently become True.
Find Candidate Binary Columns Safely
If a DataFrame already uses Boolean dtypes, pandas can identify those columns without looking at their names. For imported 0/1 data, inspect the actual non-missing values and still review the candidates before reporting them.
import pandas as pd
typed_boolean_cols = [
column
for column in df.columns
if pd.api.types.is_bool_dtype(df[column].dtype)
]
allowed = {0, 1, False, True}
zero_one_cols = [
column
for column in df.columns
if df[column].notna().any()
and set(df[column].dropna().unique()).issubset(allowed)
]
candidate_cols = sorted(set(typed_boolean_cols + zero_one_cols))
print(candidate_cols)
This finds likely binary indicator columns; it cannot determine their business meaning. A numeric field containing only 0 and 1 in today’s sample might still be a count or status code. Prefer a schema or explicit allow-list in production.
Build Counts and Percentages
For Boolean columns, sum() counts true values. mean() calculates the share of valid values that are true because pandas skips missing values by default. The summary below makes that denominator explicit.
valid_rows = indicators.notna().sum()
true_rows = indicators.sum()
false_rows = indicators.eq(False).sum()
missing_rows = indicators.isna().sum()
summary = pd.DataFrame(
{
"true_rows": true_rows,
"false_rows": false_rows,
"missing_rows": missing_rows,
"valid_rows": valid_rows,
"true_rate": indicators.mean(),
}
)
summary["true_rate_pct"] = summary["true_rate"].mul(100).round(1)
print(summary)
The official pandas DataFrame.sum documentation notes that missing values are skipped by default and exposes min_count when at least one valid value must exist. Use min_count=1 if an entirely missing indicator should return NA instead of zero.
Count True, False, and Missing with value_counts
For an audit or exploratory report, value_counts(dropna=False) is a direct way to show every state in each Boolean column. Concatenating the per-column results produces tidy output without looping over DataFrame rows.
distribution = pd.concat(
{
column: indicators[column].value_counts(dropna=False)
for column in indicator_cols
},
names=["indicator", "value"],
).rename("rows").reset_index()
print(distribution)
For one column, add normalize=True to return proportions instead of counts. Keep dropna=False when missingness is part of the question. The official pandas Series.value_counts() reference documents both options.
Decide What the Percentage Means
Two percentages can both be correct but answer different questions:
- True among valid responses: divide by non-missing rows. This is what Boolean
mean()calculates. - True among all rows: divide by the full dataset size. Missing values effectively reduce the reported rate.
valid_rate = indicators.mean()
all_row_rate = indicators.fillna(False).mean()
rate_comparison = pd.DataFrame(
{
"among_valid": valid_rate,
"among_all_rows": all_row_rate,
}
).mul(100).round(1)
print(rate_comparison)
Do not fill missing values with False merely to simplify a calculation. Do it only when the domain says that absence means no. A skipped survey response, failed sensor reading, or unavailable event stream is usually unknown, not false.
Summarize Indicators by Group
To compare segments, group the original DataFrame and aggregate only the selected indicators. Counts and rates remain separate outputs so reviewers can see sample size.
normalized = df.copy()
normalized[indicator_cols] = (
normalized[indicator_cols].astype("boolean")
)
grouped = normalized.groupby("segment")[indicator_cols]
group_counts = grouped.sum()
group_rates = grouped.mean().mul(100)
print(group_counts)
print(group_rates.round(1))
The official pandas DataFrame.groupby reference documents grouping keys and options. Small groups can produce unstable percentages, so retain group sizes and consider suppressing or flagging rates below a chosen minimum.
Count Combinations of Binary Flags
A column summary cannot show which indicators occur together. Use DataFrame.value_counts() on a small, intentional subset to count joint True/False patterns.
pattern_cols = ["opened_email", "used_search"]
pattern_counts = (
indicators[pattern_cols]
.value_counts(dropna=False)
.rename("rows")
.reset_index()
)
print(pattern_counts)
The number of possible patterns grows exponentially as columns are added, so this is useful for a focused pair or small set—not hundreds of one-hot or feature columns. For a category by one binary flag, pd.crosstab() is another readable option and supports normalized rows or columns.
Create a Long-Format Summary
A wide table is convenient for analysis; a long table is often easier to chart, filter, or export. Reset the index and name the indicator column.
long_summary = (
summary.reset_index(names="indicator")
.sort_values(["true_rate", "true_rows"], ascending=False)
)
output_columns = [
"indicator",
"true_rows",
"valid_rows",
"missing_rows",
"true_rate_pct",
]
print(long_summary[output_columns])
Export the final audit-friendly table with the techniques in Python Pool’s pandas to_csv() guide. Keep counts as integers and rates as numeric values; apply presentation formatting only at the reporting layer.
Validate the Binary Contract
Before conversion, verify that every non-missing value is permitted. Validation catches a common error in which a category code or count column is accidentally included.
allowed = {0, 1, False, True}
invalid = {
column: sorted(set(df[column].dropna()) - allowed, key=str)
for column in indicator_cols
}
invalid = {
column: values
for column, values in invalid.items()
if values
}
if invalid:
raise ValueError(f"non-binary indicator values: {invalid}")
Also check that true plus false plus missing equals total rows for every indicator. That reconciliation is a compact quality-control test and makes later changes easier to audit.
Common Mistakes
- Calling
sum()across rows when the intended summary is per column. - Treating all numeric columns as indicators.
- Reporting a percentage without its denominator.
- Converting unexpected values to Boolean before validating them.
- Replacing missing values with false without a domain rule.
- Using
apply()row by row for work that vectorized aggregation already performs.
A strong summary is not just short code. It preserves the data contract: one meaning for true, one meaning for false, and an explicit policy for unknown values.
Frequently Asked Questions
How do I count ones in multiple pandas columns?
Select the indicator columns and call sum() along the default column axis. Validate that values are genuinely binary first.
How do I calculate the percentage of true values?
Convert the indicators to Boolean and call mean(). By default, pandas excludes missing values, so also report the valid-row count.
Should missing binary values count as false?
Only when the data contract explicitly defines missing as no. Otherwise preserve missing values and show both the valid and missing counts.
How do I summarize binary columns by category?
Group by the category, select the indicator columns, and calculate both sum() for counts and mean() for rates.
How do I count True and False values in a pandas column?
Call Series.value_counts(dropna=False). Add normalize=True for proportions, and keep missing values visible when they affect the denominator.
Can pandas detect binary columns automatically?
It can identify Boolean dtypes and columns whose observed values are limited to 0 and 1, but only a schema or domain review can confirm that those columns are genuine indicators.