Track both count and running sum per category in a single loop, then derive the mean from those two accumulators. With pandas, .agg(['count', 'mean']) applies both aggregations at once and returns a DataFrame with one column per aggregation — avoiding the need to loop twice or store two separate dicts.

By hand

One loop over zip(cats, vals) fills both counts and totals. A second loop over sorted keys builds result as a {cat: (count, mean)} dict.

naive.py
Replay: real traced execution (multi-file project)
cats = ['a', 'b', 'a', 'c', 'b', 'a']
vals = [10, 20, 30, 40, 50, 60]
counts = {}
totals = {}
for cat, val in zip(cats, vals):
    counts[cat] = counts.get(cat, 0) + 1
    totals[cat] = totals.get(cat, 0) + val
result = {}
for k in sorted(counts):
    result[k] = (counts[k], round(totals[k] / counts[k], 2))
print('RESULT:', result)
  1. cats ← ['a', 'b', 'a', 'c', 'b', 'a']

    1cats = ['a', 'b', 'a', 'c', 'b', 'a']2vals = [10, 20, 30, 40, 50, 60]
    values this step['a', 'b', 'a', 'c', 'b', 'a']cats
  2. vals ← [10, 20, 30, 40, 50, 60]

    1cats = ['a', 'b', 'a', 'c', 'b', 'a']2vals = [10, 20, 30, 40, 50, 60]3counts = {}
    values this step[10, 20, 30, 40, 50, 60]vals
  3. counts ← {}

    2vals = [10, 20, 30, 40, 50, 60]3counts = {}4totals = {}
    values this step{}counts
  4. totals ← {}

    3counts = {}4totals = {}5for cat, val in zip(cats, vals):
    values this step{}totals
  5. cat ← 'a', val ← 10, counts ← {'a': 1}, totals ← {'a': 10}

    pass 1 of 6
    4totals = {}5for cat, val in zip(cats, vals):6    counts[cat] = counts.get(cat, 0) + 17    totals[cat] = totals.get(cat, 0) + val8result = {}
    values this step'a'cat10val{} {'a': 1}counts{} {'a': 10}totals
    All 6 passes — pass 1 is the card above
    passcatvalcountstotals
    1'a'10{} {'a': 1}{} {'a': 10}
    2'a' 'b'10 20{'a': 1} {'a': 1, 'b': 1}{'a': 10} {'a': 10, 'b': 20}
    3'b' 'a'20 30{'a': 1, 'b': 1} {'a': 2, 'b': 1}{'a': 10, 'b': 20} {'a': 40, 'b': 20}
    4'a' 'c'30 40{'a': 2, 'b': 1} {'a': 2, 'b': 1, 'c': 1}{'a': 40, 'b': 20} {'a': 40, 'b': 20, 'c': 40}
    5'c' 'b'40 50{'a': 2, 'b': 1, 'c': 1} {'a': 2, 'b': 2, 'c': 1}{'a': 40, 'b': 20, 'c': 40} {'a': 40, 'b': 70, 'c': 40}
    6'b' 'a'50 60{'a': 2, 'b': 2, 'c': 1} {'a': 3, 'b': 2, 'c': 1}{'a': 40, 'b': 70, 'c': 40} {'a': 100, 'b': 70, 'c': 40}
  6. for cat, val in zip(cats, vals):

    4totals = {}5for cat, val in zip(cats, vals):6    counts[cat] = counts.get(cat, 0) + 1
  7. result ← {}

    7    totals[cat] = totals.get(cat, 0) + val8result = {}9for k in sorted(counts):
    values this step{}result
  8. k ← 'a', result ← {'a': (3, 33.33)}

    pass 1 of 3
    8result = {}9for k in sorted(counts):10    result[k] = (counts[k], round(totals[k] / counts[k], 2))11print('RESULT:', result)
    values this step'a'k{} {'a': (3, 33.33)}result
    All 3 passes — pass 1 is the card above
    passkresult
    1'a'{} {'a': (3, 33.33)}
    2'a' 'b'{'a': (3, 33.33)} {'a': (3, 33.33), 'b': (2, 35.0)}
    3'b' 'c'{'a': (3, 33.33), 'b': (2, 35.0)} {'a': (3, 33.33), 'b': (2, 35.0), 'c': (1, 40.0)}
  9. for k in sorted(counts):

    8result = {}9for k in sorted(counts):10    result[k] = (counts[k], round(totals[k] / counts[k], 2))
  10. stdout ← RESULT: {'a': (3, 33.33), 'b': (2, 35.0), 'c': (1, 40.0)}

    10    result[k] = (counts[k], round(totals[k] / counts[k], 2))11print('RESULT:', result)
    values this stepRESULT: {'a': (3, 33.33), 'b': (2, 35.0), 'c': (1, 40.0)}stdout

With pandas

df.groupby('cat')['val'].agg(['count', 'mean']) returns a DataFrame indexed by category with count and mean columns. The snapshot shows the columns, index, and each aggregation's values before the final result dict.

library.py
import pandas as pd
from dalib.display import set_display
set_display()

cats = ['a', 'b', 'a', 'c', 'b', 'a']
vals = [10, 20, 30, 40, 50, 60]
df = pd.DataFrame({'cat': cats, 'val': vals})
agg = df.groupby('cat')['val'].agg(['count', 'mean'])
result = {
    k: (int(agg.loc[k, 'count']), round(float(agg.loc[k, 'mean']), 2))
    for k in sorted(agg.index)
}
print('columns:', agg.columns.tolist())
print('index:', sorted(agg.index.tolist()))
print('count:', [int(agg.loc[k, 'count']) for k in sorted(agg.index)])
print('mean:', [round(float(agg.loc[k, 'mean']), 2) for k in sorted(agg.index)])
print('RESULT:', result)
columns: ['count', 'mean']
index: ['a', 'b', 'c']
count: [3, 2, 1]
mean: [33.33, 35.0, 40.0]
RESULT: {'a': (3, 33.33), 'b': (2, 35.0), 'c': (1, 40.0)}

Implementation notes

  • .agg(['count', 'mean']) returns a DataFrame (not a Series), because there is more than one aggregation result per group. The columns are the aggregation names; the index is the group keys.
  • Passing a dict to .agg allows renaming and mixing aggregations per column: df.groupby('cat').agg({'val': ['sum', 'mean'], 'score': 'max'}).
  • int() and float() convert numpy scalars to Python natives for stable repr. agg.loc[k, 'count'] is np.int64; agg.loc[k, 'mean'] is np.float64 — both need conversion for a clean RESULT.
  • Cross-reference: group-mean (python-data-basics) for the pure-Python mean-from-accumulators version; groupby-mean (this chapter) for the single-aggregation pandas version.