Count co-occurrences of two categorical lists into a nested dict-of-dicts (a contingency table). With pandas, pd.crosstab builds this frequency table in one call, with sorted row and column labels.

By hand

With pandas

pd.crosstab(df['region'], df['status']) returns a DataFrame where each cell is the count of rows with that (region, status) combination. Row and column labels are sorted automatically.

naive.py
region = ['N', 'S', 'N', 'N', 'S', 'S']
status = ['A', 'B', 'B', 'A', 'A', 'B']
counts = {}
for i in range(len(region)):
    r = region[i]
    s = status[i]
    if r not in counts:
        counts[r] = {}
    if s not in counts[r]:
        counts[r][s] = 0
    counts[r][s] += 1
result = {k: dict(sorted(counts[k].items())) for k in sorted(counts)}
print('RESULT:', result)
library.py
import pandas as pd
from dalib.display import set_display
set_display()

region = ['N', 'S', 'N', 'N', 'S', 'S']
status = ['A', 'B', 'B', 'A', 'A', 'B']
df = pd.DataFrame({'region': region, 'status': status})
ct = pd.crosstab(df['region'], df['status'])
result = {r: {c: int(ct.loc[r, c]) for c in ct.columns} for r in ct.index}
print('index:', ct.index.tolist())
print('columns:', ct.columns.tolist())
print('A:', [int(ct.loc[r, 'A']) for r in ct.index])
print('B:', [int(ct.loc[r, 'B']) for r in ct.index])
print('RESULT:', result)
index: ['N', 'S']
columns: ['A', 'B']
A: [2, 1]
B: [1, 2]
RESULT: {'N': {'A': 2, 'B': 1}, 'S': {'A': 1, 'B': 2}}

Implementation notes

  • pd.crosstab is a frequency-count pivot — it counts rows, not aggregates values. For value aggregation, use pivot_table with aggfunc='sum' etc.
  • Pass normalize='index' to get row-fraction proportions (each row sums to 1), normalize='columns' for column fractions, or normalize='all' (same as normalize=True) to normalize over all cells.
  • Cross-reference: groupby-count (ch04) for single-column frequency counts; pivot-table-simple (this chapter) for value aggregation into a grid.