Is it possible to apply seasonal_decompose() after a groupby() where date is not the index of the data frame?

Viewed 553

So, for a forecasting project, I have a really long Dataframe of multiple time series of the following type (it has a numerical index):

date time_series_id value
2015-08-01 0 0
2015-08-02 0 1
2015-08-03 0 2
2015-08-04 0 3
2015-08-01 1 2
2015-08-02 1 3
2015-08-03 1 4
2015-08-04 1 5

My objective, is to add 3 new columns to these dataset, for each individual time series (each id) that correspond to trend, seasonal and resid. According to the characteristics of the dataset, they tend to have Nans at the start and the end of the dates.

What I was trying to do was the following:

from statsmodels.tsa.seasonal import seasonal_decompose

df.assign(trend = lambda x: x.groupby("time_series_id")["value"].transform(lambda s: s.mask(~s.isna(), other= seasonal_decompose(s[~s.isna()], model='aditive', extrapolate_trend='freq').trend))

The expected output (trend value are not actual values) should be:

date time_series_id value trend
2015-08-01 0 0 1
2015-08-02 0 1 1
2015-08-03 0 2 1
2015-08-04 0 3 1
2015-08-01 1 2 1
2015-08-02 1 3 1
2015-08-03 1 4 1
2015-08-04 1 5 1

But I get the following error message:

AttributeError: 'Int64Index' object has no attribute 'inferred_freq'

In a previous iteration of my code, this worked for my individual time series data frames, since I had embedded the date column as an index of the data frame instead of an additional column, so the "x" that the lambda function takes has already a "date time" index appropriate for the seasonal_decompose function.

df.assign(
      trend = lambda x: x["value"].mask(~x["value"].isna(), other = 
      seasonal_decompose(x["value"][~x["value"].isna()], model='aditive', extrapolate_trend='freq').trend))

My questions are, first: is it possible to achieve this using groupby? Or other approaches are possible second: is it possible to handle this that doesn't eat much memory? The original dataset I'm working on has approximately 1MM ~ rows, so any help is really welcomed :).

4 Answers

Did one of the already posed solutions work? If so or you found a different solution please share. I tried each without success, but I'm new to Python so probably missing something.

Here is what I came up with, using a for loop. For my dataset it took 8 minutes to decompose 20 million rows consisting of 6,000 different subsets. This works but I wish it were faster.

Date Time Segment ID Travel Time(Minutes)
2021-11-09 07:15:00 1 30
2021-11-09 07:30:00 1 18
2021-11-09 07:15:00 2 30
2021-11-09 07:30:00 2 17
segments = set(frame['Segment ID'])
data = pd.DataFrame([])
for s in segments:
    df = frame[frame['Segment ID'] == s].set_index('Date Time').resample('H').mean()
    comp = sm.tsa.seasonal_decompose(x=df['Travel Time(Minutes)'], period=24*7, two_sided=False) 
    df = df.join(comp.trend).join(comp.seasonal).join(comp.resid)

    #optional columns with some statistics to find outliers and trend changes
    df['resid zscore'] = (df['resid'] - df['resid'].mean()).div(df['resid'].std())
    df['trend pct_change'] = df.trend.pct_change()
    df['trend pct_change zscore'] = (df['trend pct_change'] - df['trend pct_change'].mean()).div(df['trend pct_change'].std())

    data = data.append(df.dropna())

where you have lambda x: x.groupby(..., you don't have anything to group; you are telling it to group a row (I believe). You can try a setup like this, perhaps

Here you define a function to act on the group you are sending via the apply() method. Then you should be able to use your original code.

I have not tested this, but I use this setup quite often to work on groups.

def trend_function(x):
    # do your lambda function here as you are sending each grouping
      x.assign(
      trend = lambda x: x["value"].mask(~x["value"].isna(), other = 
      seasonal_decompose(x["value"][~x["value"].isna()], model='aditive', extrapolate_trend='freq').trend))
    return x

dfnew = df.groupby('time_series_id').apply(trend_function)

use extrapolate_trend='freq' as a parameter. you add the trend, seasonal, and residual to a dictionary and plot the dictionary from statsmodels.graphics import tsaplots import statsmodels.api as sm

date=['2015-08-01','2015-08-02','2015-08-03','2015-08-04','2015-08-01','2015-08-02','2015-08-03','2015-08-04']
time_series_id=[0,0,0,0,1,1,1,1]
value=[0,1,2,3,2,3,4,5]

df=pd.DataFrame({'date':date,'time_series_id':time_series_id,'value':value})

df['date']=pd.to_datetime(df['date'])
df=df.set_index('date')
print(df)

index_day = df.index.day
value_by_day = df.groupby(index_day)['value'].mean()
fig,ax = plt.subplots(figsize=(12,4))
value_by_day.plot(ax=ax)
plt.title('value by month')
plt.show()

df[['value']].boxplot()
plt.show()

fig,ax = plt.subplots(figsize=(12,4))
df[['value']].hist(ax=ax, bins=5)
plt.show()
fig,ax = plt.subplots(figsize=(12,4))
df[['value']].plot(kind='density', ax=ax)
plt.show()

plt.clf()
fig,ax = plt.subplots(figsize=(12,4))
plt.style.use('seaborn-pastel')
fig = tsaplots.plot_acf(df['value'], lags=4,ax=ax)
plt.show()

decomposition=sm.tsa.seasonal_decompose(x=df['value'],model='additive',     extrapolate_trend='freq', period=1)
decomposition.plot()
plt.show()

decomposition_trend=decomposition.trend
ax= decomposition_trend.plot(figsize=(14,2))
ax.set_xlabel('Date')
ax.set_ylabel('Trend of time series')
ax.set_title('Trend values of the time series')
plt.show()

I changed the first piece of code according to my scinario. Here's my code and attached output

data = pd.DataFrame([])

segments = set(subset['Planning_Material'])
for s in segments:
    df = subset[subset['Planning_Material'] == s].set_index('Cal_year_month').resample('M').sum()
    comp = sm.tsa.seasonal_decompose(df) 
    df = df.join(comp.trend).join(comp.seasonal).join(comp.resid)
    df['Planning_Material'] = s
    data = pd.concat([data,df])
data = data.reset_index()
data = data[['Planning_Material', 'Cal_year_month', 'Historical_demand', 'trend','seasonal','resid']]

data

enter image description here

Related