Chức năng tổng hợp Pandas DataFrame sử dụng nhiều cột


80

Có cách nào để viết một hàm tổng hợp như được sử dụng trong DataFrame.aggphương thức, hàm đó sẽ có quyền truy cập vào nhiều cột dữ liệu đang được tổng hợp không? Các trường hợp sử dụng điển hình sẽ là trung bình có trọng số, các funcs độ lệch chuẩn có trọng số.

Tôi muốn có thể viết một cái gì đó như

def wAvg(c, w):
    return ((c * w).sum() / w.sum())

df = DataFrame(....) # df has columns c and w, i want weighted average
                     # of c using w as weight.
df.aggregate ({"c": wAvg}) # and somehow tell it to use w column as weights ...

Nice bài viết giải quyết câu hỏi này SO cụ thể: pbpython.com/weighted-average.html
ptim

Câu trả lời:


103

Đúng; sử dụng .apply(...)hàm, hàm này sẽ được gọi trên mỗi hàm con DataFrame. Ví dụ:

grouped = df.groupby(keys)

def wavg(group):
    d = group['data']
    w = group['weights']
    return (d * w).sum() / w.sum()

grouped.apply(wavg)

Có thể hiệu quả hơn nếu chia nhỏ điều này thành một vài thao tác như sau: (1) tạo cột trọng số, (2) chuẩn hóa các quan sát theo trọng số của chúng, (3) tính tổng các quan sát có trọng số được nhóm lại và tổng trọng số được nhóm lại , (4) chuẩn hóa tổng trọng số của các quan sát bằng tổng trọng số.
kalu

4
Điều gì sẽ xảy ra nếu chúng ta muốn tính toán wavg của nhiều biến (cột), ví dụ: mọi thứ ngoại trừ df ['weights']?
CPBL

2
@ Vâng, có cách nào một khi có thể làm điều này với agg()lambdađược xây dựng xung quanh np.average(...weights=...), hoặc bất kỳ hỗ trợ gốc mới nào trong gấu trúc cho các phương tiện có trọng số kể từ khi bài đăng này xuất hiện lần đầu tiên không?
sparc_spread

4
@Wes McKinney: Trong cuốn sách của mình, bạn đề xuất cách tiếp cận này : get_wavg = lambda g: np.average(g['data'], weights = g['weights']); grouped.apply(wavg) Hai cái có thể hoán đổi cho nhau không?
robroc

9

Giải pháp của tôi tương tự như giải pháp của Nathaniel, chỉ là nó dành cho một cột duy nhất và tôi không sao chép sâu toàn bộ khung dữ liệu mỗi lần, điều này có thể rất chậm. Hiệu suất đạt được trên nhóm giải pháp theo (...). Áp dụng (...) là khoảng 100x (!)

def weighted_average(df, data_col, weight_col, by_col):
    df['_data_times_weight'] = df[data_col] * df[weight_col]
    df['_weight_where_notnull'] = df[weight_col] * pd.notnull(df[data_col])
    g = df.groupby(by_col)
    result = g['_data_times_weight'].sum() / g['_weight_where_notnull'].sum()
    del df['_data_times_weight'], df['_weight_where_notnull']
    return result

Sẽ dễ đọc hơn nếu bạn sử dụng PEP8 một cách nhất quán và loại bỏ deldòng thừa .
MERose

Cảm ơn! Các deldòng thực sự là không cần thiết, kể từ khi tôi thay đổi DataFrame đầu vào tại chỗ để cải thiện hiệu suất, vì vậy tôi phải dọn dẹp.
ErnestScribebler

Nhưng bạn trả về kết quả ở dòng tiếp theo kết thúc hàm. Khi chức năng kết thúc, tất cả các đối tượng bên trong sẽ bị xóa.
MERose

1
Nhưng lưu ý rằng df không phải là một đối tượng bên trong. Đó là một đối số cho hàm và miễn là bạn không bao giờ gán cho nó ( df = something) thì nó vẫn là một bản sao cạn và được thay đổi tại chỗ. Trong trường hợp này, các cột sẽ được thêm vào DataFrame. Hãy thử sao chép-dán hàm này và chạy nó mà không có deldòng và thấy rằng nó thay đổi DataFrame đã cho bằng cách thêm cột.
ErnestScribebler

Điều này không trả lời câu hỏi, bởi vì trung bình có trọng số chỉ đóng vai trò là một ví dụ cho bất kỳ tổng hợp nào trên nhiều cột.
user__42

8

Có thể trả về bất kỳ số lượng giá trị tổng hợp nào từ một đối tượng theo nhóm với apply. Đơn giản, trả về một Chuỗi và các giá trị chỉ mục sẽ trở thành tên cột mới.

Hãy xem một ví dụ nhanh:

df = pd.DataFrame({'group':['a','a','b','b'],
                   'd1':[5,10,100,30],
                   'd2':[7,1,3,20],
                   'weights':[.2,.8, .4, .6]},
                 columns=['group', 'd1', 'd2', 'weights'])
df

  group   d1  d2  weights
0     a    5   7      0.2
1     a   10   1      0.8
2     b  100   3      0.4
3     b   30  20      0.6

Xác định một chức năng tùy chỉnh sẽ được chuyển đến apply. Nó ngầm chấp nhận một DataFrame - có nghĩa là datatham số là một DataFrame. Lưu ý cách nó sử dụng nhiều cột, điều này không thể thực hiện được với aggphương pháp groupby:

def weighted_average(data):
    d = {}
    d['d1_wa'] = np.average(data['d1'], weights=data['weights'])
    d['d2_wa'] = np.average(data['d2'], weights=data['weights'])
    return pd.Series(d)

Gọi applyphương thức groupby với chức năng tùy chỉnh của chúng tôi:

df.groupby('group').apply(weighted_average)

       d1_wa  d2_wa
group              
a        9.0    2.2
b       58.0   13.2

Bạn có thể có được hiệu suất tốt hơn bằng cách tính toán trước các tổng trọng số vào các cột DataFrame mới như được giải thích trong các câu trả lời khác và tránh sử dụng applyhoàn toàn.


4

Phần sau (dựa trên câu trả lời của Wes McKinney) hoàn thành chính xác những gì tôi đang tìm kiếm. Tôi rất vui được biết nếu có một cách đơn giản hơn để thực hiện việc này bên trong pandas.

def wavg_func(datacol, weightscol):
    def wavg(group):
        dd = group[datacol]
        ww = group[weightscol] * 1.0
        return (dd * ww).sum() / ww.sum()
    return wavg


def df_wavg(df, groupbycol, weightscol):
    grouped = df.groupby(groupbycol)
    df_ret = grouped.agg({weightscol:sum})
    datacols = [cc for cc in df.columns if cc not in [groupbycol, weightscol]]
    for dcol in datacols:
        try:
            wavg_f = wavg_func(dcol, weightscol)
            df_ret[dcol] = grouped.apply(wavg_f)
        except TypeError:  # handle non-numeric columns
            df_ret[dcol] = grouped.agg({dcol:min})
    return df_ret

Hàm df_wavg()trả về khung dữ liệu được nhóm theo cột "groupby" và trả về tổng trọng số cho cột trọng số. Các cột khác là giá trị trung bình có trọng số hoặc nếu không phải là số, thì min()hàm được sử dụng để tổng hợp.


4

Tôi làm điều này rất nhiều và thấy những điều sau đây khá hữu ích:

def weighed_average(grp):
    return grp._get_numeric_data().multiply(grp['COUNT'], axis=0).sum()/grp['COUNT'].sum()
df.groupby('SOME_COL').apply(weighed_average)

Điều này sẽ tính toán trung bình có trọng số của tất cả các cột số trong dfvà loại bỏ các cột không phải số.


Điều này là nhanh chóng! Bạn đã làm rất tốt!
Shay Ben-Sasson

Điều này thực sự hấp dẫn nếu bạn có nhiều cột. Đẹp!
Chris

@santon, cảm ơn vì câu trả lời. Bạn có thể cho một ví dụ về giải pháp của bạn? Tôi gặp lỗi 'KeyError:' COUNT 'khi cố gắng sử dụng giải pháp của bạn.
Allen

@Allen Bạn nên sử dụng bất kỳ tên nào của cột có số lượng bạn muốn sử dụng cho giá trị trung bình có trọng số.
santon

4

Hoàn thành điều này thông qua groupby(...).apply(...)là không hiệu quả. Đây là một giải pháp mà tôi sử dụng mọi lúc (về cơ bản là sử dụng logic của kalu).

def grouped_weighted_average(self, values, weights, *groupby_args, **groupby_kwargs):
   """
    :param values: column(s) to take the average of
    :param weights_col: column to weight on
    :param group_args: args to pass into groupby (e.g. the level you want to group on)
    :param group_kwargs: kwargs to pass into groupby
    :return: pandas.Series or pandas.DataFrame
    """

    if isinstance(values, str):
        values = [values]

    ss = []
    for value_col in values:
        df = self.copy()
        prod_name = 'prod_{v}_{w}'.format(v=value_col, w=weights)
        weights_name = 'weights_{w}'.format(w=weights)

        df[prod_name] = df[value_col] * df[weights]
        df[weights_name] = df[weights].where(~df[prod_name].isnull())
        df = df.groupby(*groupby_args, **groupby_kwargs).sum()
        s = df[prod_name] / df[weights_name]
        s.name = value_col
        ss.append(s)
    df = pd.concat(ss, axis=1) if len(ss) > 1 else ss[0]
    return df

pandas.DataFrame.grouped_weighted_average = grouped_weighted_average

1
Khi bạn nói người không biểu diễn. Mức chênh lệch là bao nhiêu? Đã đo nó?
Bouncner
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.