Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Data aggregation and group operations

import numpy as np
import pandas as pd

Data aggregation and group operations

  • After loading, merging, cleaning a dataset

    • compute group statistics
  • pandas (like SQL) has flexible operations for joining, filtering, aggregating data

GroupBy mechanics

  • Group operations are also called split-apply-combine

    1. split data (from DataFrame or Series) into groups:
      • based on certain keys
      • along rows or columns
    2. apply a function to each group producing a new value
      • E.g., sum()
    3. combine the results into a series / df object
  • Grouping can happen in many ways:

    • list or array with values encoding the groups (same length as the axis being grouped)
    • a dict or a Series giving the correspondence between values on the axes and group names
    • the name of the column to be used for the split
    • a function invoked on the index or on the rows / columns
np.random.seed(10)

df = pd.DataFrame({
    'key1': ['a', 'a', 'b', 'b', 'a'],
    'key2': ['one', 'two', 'one', 'two', 'one'],
    'data1': np.random.randn(5),
    'data2': np.random.randn(5)
})

df
Loading...
# 1)
# - We want to compute the mean of the values in "data1" grouping by values of "key1"
# - groupby() computes the mapping between keys of the groups and rows of the dataframe
grouped = df['data1'].groupby(df['key1'])

# We are grouping a Series since we have a single column.
grouped
<pandas.core.groupby.generic.SeriesGroupBy object at 0x110a3e048>
grouped2 = df[['data1', 'data2']].groupby(df['key1'])

# We are grouping a DataFrame since we have two columns.
grouped2
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x110a3e1d0>
# For each value of the group we compute the mean of the corresponding rows.
grouped.mean()
key1 a 0.889400 b -0.776892 Name: data1, dtype: float64
# If we group by 2 keys, we end up with a hierarchical index Series.
means = df['data1'].groupby([df['key1'], df['key2']]).mean()

means
key1 key2 a one 0.976461 two 0.715279 b one -1.545400 two -0.008384 Name: data1, dtype: float64
means.unstack()
Loading...
df['data1']
0 0.958372 1 0.562044 2 0.359613 3 -1.723603 4 -0.895805 Name: data1, dtype: float64
# 2)
# We can also use arrays to infer the groups, as long as the size is
# the same as the number of rows.
states = np.array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'])
years = np.array([2005, 2005, 2006, 2005, 2006])

print(len(states), len(years), len(df['data1']))

df['data1'].groupby([states, years]).mean()
5 5 5
California 2005 0.715279 2006 -1.545400 Ohio 2005 0.661601 2006 0.621336 Name: data1, dtype: float64
# 3)
# The grouping information can be stored in the same data frame as the data
# Note that df['key2'] is excluded from the mean since it is not numerical.
df.groupby('key1').mean()
Loading...
# Group by 2 keys the entire df.
df.groupby(['key1', 'key2']).mean()
Loading...
# We can count the number of elements with size().
# Note that nan are excluded.
df.groupby(['key1', 'key2']).size()
key1 key2 a one 2 two 1 b one 1 two 1 dtype: int64

Iterating over groups

  • groupby object supports iteration.
df
Loading...
for name, group in df.groupby('key1'):
    # group is the dataframe.
    print("\n# key=", name)
    print("group=\n", group)

# key= a
group=
   key1 key2     data1     data2
0    a  one  1.331587 -0.720086
1    a  two  0.715279  0.265512
4    a  one  0.621336 -0.174600

# key= b
group=
   key1 key2     data1     data2
2    b  one -1.545400  0.108549
3    b  two -0.008384  0.004291
# In case of grouping by multiple keys, the "key" is a tuple of values.
for (k1, k2), group in df.groupby(['key1', 'key2']):
    print("\n# key=", (k1, k2))
    print("group=\n", group)

# key= ('a', 'one')
group=
   key1 key2     data1     data2
0    a  one  1.331587 -0.720086
4    a  one  0.621336 -0.174600

# key= ('a', 'two')
group=
   key1 key2     data1     data2
1    a  two  0.715279  0.265512

# key= ('b', 'one')
group=
   key1 key2   data1     data2
2    b  one -1.5454  0.108549

# key= ('b', 'two')
group=
   key1 key2     data1     data2
3    b  two -0.008384  0.004291
# One can compute a dict out of the groupby in one line.
pieces = dict(list(df.groupby('key1')))

import pprint

pprint.pprint(pieces)
{'a':   key1 key2     data1     data2
0    a  one  1.331587 -0.720086
1    a  two  0.715279  0.265512
4    a  one  0.621336 -0.174600,
 'b':   key1 key2     data1     data2
2    b  one -1.545400  0.108549
3    b  two -0.008384  0.004291}

Selecting a column or subset of columns

df
Loading...
# Group by "key1".
grouped = df.groupby('key1')

# .groups.keys() to get the keys.
print("keys=", list(grouped.groups.keys()))

# A groupby object can be split by column after being computed.
print(df.groupby('key1')["data1"])
keys= ['a', 'b']
<pandas.core.groupby.generic.SeriesGroupBy object at 0x1109f8780>
print(df.groupby('key1')["data1"].mean())
print(df.groupby('key1')["data2"].mean())
key1
a    0.889400
b   -0.776892
Name: data1, dtype: float64
key1
a   -0.209725
b    0.056420
Name: data2, dtype: float64
# It is equivalent to
# - "split and then select" and
# - "select and then split"
print(df.groupby('key1')["data1"].mean())

# Once we have selected "data1" there is no "key1" anymore so we use the array
# df["key1"] to label the values and group.
print(df["data1"].groupby(df['key1']).mean())
key1
a    0.889400
b   -0.776892
Name: data1, dtype: float64
key1
a    0.889400
b   -0.776892
Name: data1, dtype: float64

Grouping with dicts and series

np.random.seed(10)

people = pd.DataFrame(
    np.random.randn(5, 5),
    columns=list("abcde"),
    index="Joe Steve Wes Jim Travis".split())

# Add NAs at row = 2 and columns = [1, 3]
people.iloc[2:3, [1, 3]] = np.nan

people
Loading...

Finish

# Build a map from columns to group and aggregate.
mapping = {
    'a': 'red',
    'b': 'red',
    'c': 'blue',
    'd': 'blue',
    'e': 'red',
    'f': 'orange'
}

by_column = people.groupby(mapping, axis=1)

by_column.sum()
Loading...
# Transform the dict into a fixed mapping series.
map_series = pd.Series(mapping)

print(map_series)

display(people.groupby(map_series, axis=1).sum())
a       red
b       red
c      blue
d      blue
e       red
f    orange
dtype: object
Loading...

Grouping with functions

  • Instead of a fixed mapping through dict or Series, a function can be used
  • When passing a function to groupby(), the function is called on the index and the result is the group
# Group by length of name
people.groupby(len).sum()
Loading...

Grouping by index level

  • One can use the hierarchical index to aggregate using one of the levels
columns = pd.MultiIndex.from_arrays(
    [['US', 'US', 'US', 'JP', 'JP'], [1, 3, 5, 1, 3]],
    names=['cty', 'tenor'])

columns
MultiIndex([('US', 1), ('US', 3), ('US', 5), ('JP', 1), ('JP', 3)], names=['cty', 'tenor'])
hier_df = pd.DataFrame(np.random.randn(4, 5), columns=columns)

hier_df
Loading...
hier_df.groupby(level='cty', axis=1).count()
Loading...

Data aggregation

  • aggregation = transformation from arrays to scalar value
    • E.g., mean, count, min, sum, first, last
df
Loading...
# split by values of key1 and compute quantile.
df.groupby('key1').quantile(0.9)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-31-28b15e947493> in <module>
      1 # split by values of key1 and compute quantile.
----> 2 df.groupby('key1').quantile(0.9)

~/.conda/envs/study/lib/python3.7/site-packages/pandas/core/groupby/groupby.py in quantile(self, q, interpolation)
   1906             post_processing=post_processor,
   1907             q=q,
-> 1908             interpolation=interpolation,
   1909         )
   1910 

~/.conda/envs/study/lib/python3.7/site-packages/pandas/core/groupby/groupby.py in _get_cythonized_result(self, how, grouper, aggregate, cython_dtype, needs_values, needs_mask, needs_ngroups, result_is_index, pre_processing, post_processing, **kwargs)
   2236                 vals = obj.values
   2237                 if pre_processing:
-> 2238                     vals, inferences = pre_processing(vals)
   2239                 func = partial(func, vals)
   2240 

~/.conda/envs/study/lib/python3.7/site-packages/pandas/core/groupby/groupby.py in pre_processor(vals)
   1873             if is_object_dtype(vals):
   1874                 raise TypeError(
-> 1875                     "'quantile' cannot be performed against " "'object' dtypes!"
   1876                 )
   1877 

TypeError: 'quantile' cannot be performed against 'object' dtypes!
# One can use any custom function.
def peak_to_peak(arr):
    return arr.max() - arr.min()

df.groupby('key1').agg(peak_to_peak)
# Also functions like describe() work, although they are not
# aggregations.

df.groupby('key1')["data1"].describe()
df.groupby('key1').describe()

Column-wise and multiple function application

tips = pd.read_csv('~/src/github/pydata-book/examples/tips.csv')

tips['tip_pct'] = tips['tip'] / tips['total_bill']

tips.head()
grouped = tips.groupby(['day', 'smoker'])

# Select a column.
grouped_pct = grouped['tip_pct']
print("keys=", list(grouped_pct.groups.keys()))

# Equivalent.
grouped_pct.agg('mean')
grouped_pct.mean()
# Pass a list of aggregation functions.
funcs = ['mean', 'std', peak_to_peak]
grouped_pct.agg(funcs)
# Assign name to the functions.
funcs = [
    ('foo', 'mean'),
    ('bar', np.std)
]
grouped_pct.agg(funcs)
funcs = ['count', 'mean', 'max']
result = grouped['tip_pct', 'total_bill'].agg(funcs)

# It has hierarchical columns for both rows and columns.
result
# Aggregation functions can also be specified by dict.
funcs = {'tip': np.max, 'size': 'sum'}
grouped.agg(funcs)
funcs = {'tip_pct': ['min', 'max', 'mean', 'std'], 'size': 'sum'}
grouped.agg(funcs)

Returning aggregated data without row indexes

tips.groupby(['day', 'smoker'], as_index=True).mean()
# Returning a hierarchical index can be disabled.
tips.groupby(['day', 'smoker'], as_index=False).mean()
# This is equivalent to call reset_index().

tips.groupby(['day', 'smoker'], as_index=True).mean().reset_index()

Apply: General split-apply-combine

# You want to select the top five tip_pct values by group.

def top(df, n=2, column='tip_pct'):
    return df.sort_values(by=column)[-n:]

top(tips, n=6)
# top() is called on each row group and then results
# are concat with pandas.concat, using labels from group name.
tips.groupby('smoker').apply(top)
# You can pass params to the function using **kwargs.
tips.groupby('smoker').apply(top, n=1, column='total_bill')
# This operation is what describe() does.
display(tips.groupby('smoker')["tip_pct"].describe())

df2 = tips.groupby('smoker')["tip_pct"].apply(lambda x: x.describe())
display(df2)

Suppressing the group keys

# Disable the hierarchial indexing.

df = tips.groupby('smoker', group_keys=True).apply(top)
display(df)

df = tips.groupby('smoker', group_keys=False).apply(top)
display(df)

Quantile and bucket analysis

df = pd.DataFrame({
    "data1": np.random.randn(100),
    "data2": np.random.randn(100)
})

display(df.head())
quartiles = pd.cut(df.data1, 4)

print(type(quartiles))

quartiles[:4]
# We can use the series above to groupby.

# We can filter by data2 since we already know the mapping.
grouped = df["data2"].groupby(quartiles)

for k, v in grouped:
    print(k)
    print(v.head(2))
def get_stats(group):
    #print group
    #assert 0
    return pd.Series({
        'min': group.min(),
        'max': group.max(),
        'count': group.count(),
        'mean': group.mean()
    })

df2 = grouped.apply(get_stats)

display(df2)

# Move one level of index to columns.
df2.unstack()
pd.qcut(df.data1, 4).head()
pd.cut(df.data1, 4).head()

Example: filling missing values with group-specific values

s = pd.Series(np.random.randn(6))
s[::2] = np.nan

s
s.fillna(s.mean())
# One can fill nans based on the group.

states = [
    'Ohio', 'New York', 'Vermont', 'Florida', 'Oregon', 'Nevada', 'California',
    'Idaho'
]
group_key = ['East'] * 4 + ['West'] * 4
data = pd.Series(np.random.randn(8), index=states)

data
data[['Vermont', "Nevada", "Idaho"]] = np.nan

data
data.groupby(group_key).mean()
fill_mean = lambda g: g.fillna(g.mean())

data.groupby(group_key).apply(fill_mean)

Example: random sampling and permutation

# Hearts
# Spades
# Clubs
# Diamonds
suits = list("HSCD")
# Values.
card_val = list(list(range(1, 10 + 1)) + [10] * 3)
base_names = ['A'] + list(range(2, 10 + 1)) + list("JQK")
assert len(card_val) == len(base_names)

cards = []
for suit in suits:
    cards.extend(str(num) + suit for num in base_names)

deck = pd.Series(card_val * 4, index=cards)
assert len(deck) == 52
deck.head()
# Draw two cards without replacement.

def draw(deck, n=5):
    return deck.sample(n)


draw(deck)
# Last letter is suit.
get_suit = lambda card: card[-1]
# Draw 2 cards per suit.
# Group by suit, and then get 2 cards from each group.
deck.groupby(get_suit, group_keys=False).apply(draw, n=2)

Example: group weighted average and correlation

df = pd.DataFrame({
    'category': ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'b'],
    'data': np.random.randn(8),
    'weights': np.random.rand(8)
})

df
grouped = df.groupby('category')

# Aggregation function: dot product between data and weights.
get_wavg = lambda g: np.average(g['data'], weights=g['weights'])

grouped.apply(get_wavg)
close_px = pd.read_csv(
    '~/src/github/pydata-book/examples/stock_px_2.csv',
    parse_dates=True,
    index_col=0)

close_px.head()
close_px.info()
close_px.describe()
# Compute rets.

rets = close_px.pct_change().dropna()

rets.head()
# For each stock compute the correlation with SPX.
spx_corr = lambda x: x.corrwith(x['SPX'])

# Groupby year.
get_year = lambda x: x.year

# For each year, compute the correlation of each stock to SPX.
by_year = rets.groupby(get_year)
by_year.apply(spx_corr)
# For each year, compute the correlation of AAPL and MSFT.
by_year.apply(lambda x: x['AAPL'].corr(x['MSFT']))

Example: group-wise linear regression

  • You can use groupby to perform more complex analysis, as long as function returns a pandas object (Series or DataFrame) or scalar value
import statsmodels.api as sm

def regress(data, yvar, xvars):
    Y = data[yvar]
    X = data[xvars]
    X['intercept'] = 1.0
    result = sm.OLS(Y, X).fit()
    return result.params

by_year.apply(regress, 'AAPL', ['SPX'])

Pivot tables and cross-tabulation

  • A pivot table aggregates a table of data by one or more keys arranging results for rows and columns
tips.head()
# Aggregate through mean by two indices.
tips.pivot_table(index=['day', 'smoker'])
#?tips.pivot_table
# Compute two metrics by
# - 3 vars: 2 on the index and 1 on the columns
tips.pivot_table(['tip_pct', 'size'],
                 index=['time', 'day'],
                 columns='smoker')
# We can also add summation over each var, so that
# there are values for 2 variables.
tips.pivot_table(['tip_pct', 'size'],
                 index=['time', 'day'],
                 columns='smoker',
                 margins=True)
# You can specify the aggregation function by passing aggfunc.
tips.pivot_table('tip_pct',
                 index=['time', 'smoker'],
                 columns='day',
                 aggfunc=len, margins=True)

Crosstab

  • Special case of pivot table that computes group frequencies
pd.crosstab([tips.time, tips.day], tips.smoker, margins=True)q