How can I get a list of regular entries of the same size for pandas dataframe?

Viewed 47

I have a dataframe with the columns [date, name, size]

I'd like to get a dataframe with the columns

[name, type, size, dates]

1 Answers

setting the tolerate-level for amount

threshold = 0.02

Some pre-processing on the date to extract useful columns

df['date'] = pd.to_datetime(df['date'])
df['amount'] = df['amount'].astype(float)
df['str_date'] = df['date'].dt.date.astype(str)
df['dayofmonth'] = df['date'].dt.day
df['month'] = df['date'].dt.year*12 + df['date'].dt.month
df['dayofweek'] = df['date'].dt.dayofweek
df['weeknum'] = (df['date'] - pd.to_datetime('2022-02-06')).dt.days // 7

Define helper functions.

import numpy as np from itertools import groupby

def splitter(seq, threshold):
    i, amount_base = 0, seq[0][1]
    
    for j, r in enumerate(seq):
        if abs(r[1]/amount_base-1) > threshold:
            if j - i >= 3:
                yield i
            i, amount_base = j, r[1]

def cont_seqs(df, column, threshold):
    '''column: 'weeknum' for weekly, 'month' for monthly'''

    # weekly or hourly continuity
    df['continuity'] = df[column].values - np.arange(len(df))
    seqs = [
        list(map(lambda r: (r.str_date, r.amount) , rs)) 
            for _, rs in groupby(df.itertuples(), lambda r: r.continuity)
    ]
    
    # amount threshold and minimum length
    return [
        dict(zip(('dates', 'amount'), list(zip(*segment))))
            for seq in seqs 
                for segment in np.split(seq, list(splitter(seq, threshold))) 
                    if len(segment) >= 3
    ]

First, we will discover all the weekly patterns.

Because a weekly pattern shares the same dayofweek (e.g. they are all Wednesday), the idea is to groupby the dataframe with name and dayofweek.

The next step is to find out, within a group, all valid sequences of dates that are "weekly-continuous". We use continuous_sequences(...) for this. A valid sequence is a sequence that has >=3 dates, and all amount values are within tolerate-level with respect to the first amount.

Since a group can contain more than one valid sequences, we do explode so that one sequence take up one row. However, a group can have no valid sequence at all so we will use dropna to remove them.

The reset of the methods take care of the formatting.

weekly = df.groupby(['name', 'dayofweek'])\
.apply(lambda df: list(cont_seqs(df, 'weeknum', .02)))\
.explode()\
.dropna()\
.apply(pd.Series)\
.reset_index()\
.drop(columns='dayofweek')\
.assign(recurring_type='weekly')\
.reindex(['name', 'recurring_type', 'amount', 'dates'], axis=1)

Then, we will discover all the monthly patterns.

This time, dates in a monthly pattern should share the same dayofmonth which is why we put it into our groupby. We follow a similar logic as what we did for the weekly pattern.

monthly = df.groupby(['name', 'dayofmonth'])\
.apply(lambda df: list(cont_seqs(df, 'month', .02)))\
.explode()\
.dropna()\
.apply(pd.Series)\
.reset_index()\
.drop(columns='dayofmonth')\
.assign(recurring_type='monthly')\
.reindex(['name', 'recurring_type', 'amount', 'dates'], axis=1)

Finally, we combine the two kinds of patterns into one dataframe.

pd.concat([weekly, monthly]).reset_index(drop=True)

Result

name    recurring_type  amount  dates
0   John    weekly  (10.0, 10.0, 10.0)  (2021-07-01, 2021-07-08, 2021-07-15)
1   John    monthly (10.0, 10.04, 10.0) (2021-10-01, 2021-11-01, 2021-12-01)
Related