3
\$\begingroup\$

I want to calculate the quarterly average of a time-indexed dataframe column in a rolling fashion. The mean at any timestamp should not contain information about future timestamps.

This is a code to test the function I wrote. Is there a way to optimize it?

import pandas as pd
import numpy as np
import time


date_range = pd.date_range(start="2020-01-01", end="2023-12-31", freq='D')
np.random.seed(0) 
volume_data = np.random.randint(100, 1000, size=len(date_range))
df = pd.DataFrame({'Date': date_range, 'Volume': volume_data})
df.set_index('Date', inplace=True)


def q_mean(df):
    avg = []
    for t in range(200, len(df)):
        
        q = df.index[t].quarter
        df_filt = df[(df.index.quarter == q) & (df.index < df.index[t])]
        avg.append(df_filt['Volume'].mean())

    return avg

start_time = time.time()
result = q_mean(df)
duration = time.time() - start_time
\$\endgroup\$
3
  • \$\begingroup\$ Are you sure that your implementation is correct? The initial conditions don't make any sense to me. You start 200 indices in to select your first date, but you pull in rows for your mean that are well before that index; and that's the only quarter that works like that; the other quarters are full. \$\endgroup\$
    – Reinderien
    Commented Nov 10 at 22:12
  • \$\begingroup\$ I also can't see how an exclusive lesser-than df.index < df.index[t] is valid in context; I would expect <=. \$\endgroup\$
    – Reinderien
    Commented Nov 10 at 22:23
  • \$\begingroup\$ < is correct for the problem at hand. I am not sure about the "initial condition". If it refers to 200, it was just for sake of testing at some point, apologies. It should work also before that point. \$\endgroup\$
    – shamalaia
    Commented Nov 11 at 20:20

2 Answers 2

3
\$\begingroup\$

Put aside optimisation: this is probably just wrong. It has a NaN on index 74 where there probably shouldn't be one. I don't trust the implementation, especially in the neighbourhood of the starting condition 200:.

I would delete everything and replace it with a single Series (not a dataframe) to calculate

return volume.groupby(
    pd.Grouper(level=0, freq='QS')
).expanding().mean()
\$\endgroup\$
2
  • \$\begingroup\$ The reason I wrote the function was actually to check a similar solution I came up with after some time on pandas docs (not my forte): ` df['Quarter'] = df.index.quarter cum_mean = ( df.groupby('Quarter')['Volume'] .expanding() .mean() .reset_index(level=0, drop=True) )` Which did not convince me because of some sudden jumps at the beginning of new quarters also quite "late" in the series (when it should have enough past data to calculate something more meaningful). \$\endgroup\$
    – shamalaia
    Commented Nov 11 at 20:40
  • \$\begingroup\$ I totally missed the NaN btw. I was convinced that the new solution was exact because after some time it has a some more smooth behavior which I would expect from uniform data. \$\endgroup\$
    – shamalaia
    Commented Nov 11 at 20:42
1
\$\begingroup\$

Thank you for offering a reproducible reprex, I appreciate that.

problem definition

calculate the quarterly average ... in a rolling fashion.

Months in the Gregorian calendar do not, alas, all have 30 days. But we won't soon be going back to the five intercalary days of the Revolutionary calendar.

It isn't clear from the review context if there is some regulatory requirement you are trying to conform to. You may find it more convenient to rephrase "quarterly average" as "90-day average". That would let you use the numpy concept of a sliding window, or the excellent support for rolling windows offered by pandas or polars.

caching

Computing (df.index.quarter == q) hundreds and hundreds of times is just silly, given that the current t is in the same old q > 98% of the time. Numpy must examine more than 1400 rows each time. Better to cache the result, and only recompute when t advances into a new quarter.

But that brings us to the sticking point of the final quarter. If there's just a handful of days, do we really want to compute a "smoothed" mean over just a tiny sample?

stability

The OP code proposes μ = 550 for the random variable Volume, which is a stationary process. But imagine that the Generating Process was four light trading days centered on 450, followed by four heavy trading days with mean 650, and then the cycle repeats. In the opening days of a new quarter, do we really want to assert the long-term mean has moved below 500, or above 600? We average across multiple days in order to reduce the variance -- σ for a 3-day sample will be greater than σ for a 90-day sample. The OP code essentially offers large σ in the opening days of the current quarter, with σ dwindling as we move further into the quarter.

specification

If there is a written Requirements document, perhaps part of GAAP rules, then mention that in a """docstring""" or comment. Absent such constraints, I advise you to define a "90-day window" requirement, and implement it with support from the libraries I mentioned above.

\$\endgroup\$
2
  • \$\begingroup\$ By quarterly average I mean also including data in the same quarter from past years. So it is not really 90 days. The problem is related to time series backtesting. I see it as a rolling estimate, apologies if it not a rigorous terminology. I am not sure how to cache data if at every iteration the value of the mean can be changed by some outliers intervened in the precedent day. Sorry if there is something I miss. I do not get your point about the stability. Honestly, I just made up some data to check that another solution similar to @Reinderien returned the same results as this one. \$\endgroup\$
    – shamalaia
    Commented Nov 11 at 20:28
  • \$\begingroup\$ Ooohhhh, there is a seasonality effect you wish to capture. Hmm. I did not see that at all, in the code nor in the Review Context. Ok, that is certainly a fair goal to shoot for. \$\endgroup\$
    – J_H
    Commented Nov 12 at 5:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.