Существует проблема с високосными годами, поэтому повторная выборка или Timedelta
невозможна, проще всего использовать цикл с диапазоном по минимальным и максимальным годам и выделением с помощью f-строк:
np.random.seed(2019)
rng = pd.date_range('2015-03-21 14:00:00', '2018-03-21 14:30:00', freq='10T')
df = pd.DataFrame({'speed':np.random.randint(1000, size=len(rng))}, index=rng)
#print (df)
out = pd.Series({x: df.loc[f'{x}-03-21 14:00:00':f'{x+1}-03-21 13:50:00', 'speed'].mean()
for x in range(df.index.year.min(), df.index.year.max()+1)})
print (out)
2015 501.062879
2016 498.546385
2017 498.490963
2018 580.250000
dtype: float64
Другое решениеболее сложный, но хорошо работающий с високосными годами - идея разбивается каждый год на 2 части - до даты и времени, а затем суммируется.
#datetime for thresh - always need leeap year like 2000
date = pd.Timestamp('2000-03-21 14:00:00')
#replace all years to 2000 and test data fr matched conditions
mask = pd.to_datetime(df.index.strftime('2000-%m-%d %H:%M:%S')) < date
arr = np.where(mask, 'matched','nonmatched')
#sum of means have no sense, so need working mean = sum/count
df1 = df.groupby([arr, df.index.year])['speed'].agg(['sum','size'])
print (df1)
sum size
matched 2016 5811589 11604
2017 5725034 11460
2018 5702078 11460
nonmatched 2015 20596429 41100
2016 20478564 41100
2017 20498607 41100
2018 2321 4
#data before thresh datetime
matched = df1.loc['matched']
matched.index -= 1
print (matched)
sum size
2015 5811589 11604
2016 5725034 11460
2017 5702078 11460
#data after thresh
nonmatched = df1.loc['nonmatched']
print (nonmatched)
sum size
2015 20596429 41100
2016 20478564 41100
2017 20498607 41100
2018 2321 4
#sum both DataFrames and divide sum by counts for mean
df2 = matched.add(nonmatched, fill_value=0)
out = df2['sum'].div(df2['size'])
print (out)
2015 501.062879
2016 498.546385
2017 498.490963
2018 580.250000
dtype: float64