Time Series Forecast in Python. End to End Time Series Forecast using... | by Songhao Wu | Towards Data Science

Since the start of Covid and the lockdown, walking out freely has been a luxury for many people including myself. Personally, I have been trying to walk away from my laptop and go out for some fresh air each day. Each week, I would look at my step count from my phone and see whether I have been exercising enough.
How cool would it be if we can accurately predict our step count for the following week!
In this article, I will use basic time series analysis to look at the trend of step count for myself and also predict the future using historical step count. The main purpose of this article is to show the main idea of time series forecast and use my own step count as the practical coding example.
I am using the Step Count data downloaded from my iPhone Apple Health. If you are also using iPhone you can download the health data from your Apple Health application profile page as shown below
The downloaded data would be an XML file that contains multiple sectors of Apple Health including Step Count, Heart Rate, Sleep Analysis, etc. I utilized the script here from the link to convert the XML file to CSV format for analysis. The output of the file would be multiple CSV files and you can use Python to directly connect to the CSV file. It is also a fun data source for you to explore other ways of data analysis or data visualization using your own health data.
The step count data looks like the screenshot below which contains the step count value each time when you move. It also contains the start date, end date, and creation date. Here creation date is referring to the creation date&time of this record and thus I am using the start date to record the time.
I would only require the two columns ‘startDate’ and ‘Value’ for my analysis. The first step for me is to convert the date column into date time format and then aggregate the data into a weekly sum. This is because many python functions or packages for time series would require one column on time format and if I predict the data on daily basis the volatility would be too high.
#read data from extracted csvsteps=pd.read_csv('apple_health_export/StepCount.csv')#convert start date into time formatsteps['date']=pd.to_datetime(steps['startDate'].str[:19])#Aggregate data into weekly sumsample=steps[['date','value']]weekly=sample.resample('W', on='date').sum()#visualize weekly dataweekly.plot(figsize=(15, 6))plt.show()
From the visualization above, we can see that even weekly step count data has very high volatility. To remove the effect of extreme values like 300k step count per week or less than 2000 steps a week, I use winsorization to make the data more normal.
Winsorization: Transformation of the data by limiting the extreme value to a certain boundary in order to remove the effect of outliers.
I do not directly winsorize on the weekly level but rather limit on the daily step count level. For example, if I walk too much on one day, the daily step count would be lowered into the upper boundary value and then I aggregate those winsorized daily steps into the weekly sum again.
I choose the boundary limit to be more top 2% and bottom 2%(this percentage value can be determined by us based on the distribution of the data). I use the ‘winsorize’ Python package in which you can directly specify the upper and lower percentage limit.
#remove the first part of data with no stepssample=steps[steps['date']>'2015-09-01'][['date','value']]#aggregate data on daily leveldaily=sample.resample('D',on='date').sum()#Winsorize daily datadaily['winsorized_value']=winsorize(daily['value'], limits=[0.02, 0.02])#Aggregate daily data into weekly data againweekly=daily.resample('W').sum()#visualize new weekly dataweekly.plot(figsize=(15, 6))plt.show()
You can see from the visualization above that the winsorized data has less extreme values and thus gives us a more general part. However, one thing to note that this winsorization step is not compulsory because most time series methods would have some other smoothing steps.
You can also look at the seasonality pattern and overall trend by using the seasonal_decompose package. The trend here is determined by the moving average and then the seasonal element is extracted after removing the trend from the original data.
extract=train.set_index('date')from pylab import rcParamsrcParams['figure.figsize'] = 18, 8decomposition = sm.tsa.seasonal_decompose(extract, model='additive')fig = decomposition.plot()plt.show()
You can see from the graph above that there is some slight seasonal element in the step count. There is a strong movement in the overall trend especially since the start of the covid period.
After we explored the data, we can start the prediction using different time series forecast methods.
Problem Statement: How accurate can I predict next week step count based on historical step count data?
Data preparation: The step is slightly different from the normal machine learning train-test split. This is because training data and testing data are dynamic depending on the week you are predicting(we use all historical data before that week as training data). We are using step count in 2021 as the benchmark to measure the accuracy of our model.
#determine training and testing groupdata=weekly.reset_index()test=data[data['date']>'2021-01-01'][['date','value']]train=data[['date','value']]
We will use the Mean Average Error(MAE) to measure the accuracy of all the models. You can choose ‘mean square error’, ‘root mean square error’ or ‘mean average error percentage’ to measure as well. If you want to understand more about measuring the regression or prediction model, you can refer to here.
Moving Average simply means the average of previous X periods and X is for you to determine. This is one of the most commonly used techniques/concepts for time series data. It can help to smooth out the data trend and thus give you a more realistic approximation of the result.
In my example here, I am using the past 10 time periods moving average which means I feel the step count is mostly affected by the step count for the previous 10 weeks. You can choose to use a different number. The common time periods used in moving average are 10,20,30,50,100 depending on the scenario.
#get rolling moving average for previous 10 weeksdata['SMA_10']=train['value'].rolling(window=10).mean().shift(1)#Measure the MAE for this measuretest=data[data['date']>'2020-12-31']from sklearn.metrics import mean_absolute_errorprint(mean_absolute_error(test['value'], test['SMA_10']))#10415.972
Method 1 moving average is the simple average of the past 10 weeks. You may want to say that week closer to the prediction date should carry a more heavy weightage rather than a simple average. That is when we can use the exponentially weighted moving average.
Exponentially Weighted Moving Average gives closer date a bigger weightage and the allocation of weightage follows exponential logic. Let's see how it performs against the simple moving average method.
#Calculate ewm average valuedata['ewma_10']=train['value'].ewm(span=10).mean().shift(1)test=data[data['date']>'2020-12-31']from sklearn.metrics import mean_absolute_errorprint(mean_absolute_error(test['value'], test['ewma_10']))#9613.11
The exponentially weighted moving average gives a better result of lower mean average error compared to simple moving average.
test_plot_data=test.set_index('date')[['SMA_10','ewma_10']]train_plot=train[train['date']>'2020-01-01'].set_index('date')plt.figure(figsize=(15,10))plt.plot(train_plot,label='Actual Step')plt.plot(test_plot_data['SMA_10'],label='sma_10')plt.plot(test_plot_data['ewma_10'],label='ewma_10')plt.legend(loc='Left corner')plt.show()
If you plot the two predictions vs the real step count value, you can see that the prediction from both moving average approaches is much more smooth than the real step counts. Exponentially Weighted Moving Average(yellow line) captures the trend faster and has a closer fit to the real step count compared to simple moving average(red line).
SARIMA model is the ARIMA model with the seasonality trend. The full name of the ARIMA model is Auto-Regressive Integrated Moving Average. Before I start on the method, let us have a quick walk-through of what exactly is ARIMA and what are factors we consider when we model it.
Quick Summary:
Auto-Regressive/AR: The output prediction is dependent on the previous observations/lagged observations
Integrated/I: The data is not stationary and it requires certain order of differencing to achieve the stationarity
Moving Average/MA: The output prediction is dependent on previous error terms/lagged errors
For the SARIMA model, you need to specify 7 different parameters:
p : number of lag for AR term
d : number of times of differencing required to achieve the stationarity
q : number of lag for MA term
s : seasonality periods. Refers to the number of time periods that the same pattern occurs again.
P,D,Q : same as p,d,q but it is parameters for seasonality side
There are articles online on how we can determine different parameters based on the observation of the data. For this method, I am using the package from statsmodel.
For each model you run, you could have a result summary and an AIC value to measure the in-sample fitness of the model. I use an iteration of different values for all the parameters and find out the lowest AIC value(best fit model for in-sample data). I learned this method from the link here.
# Define the p, d and q parameters for value between 0-2 and iterate for all the value in the rangep = d = q = range(0, 2)pdq = list(itertools.product(p, d, q))# Generate all different combinations of seasonal p, q and q seasonal_pdq = [(x[0], x[1], x[2], 4) for x in list(itertools.product(p, d, q))]params=[]seasonal=[]aic=[]for param in pdq:    for param_seasonal in seasonal_pdq:        mod = sm.tsa.statespace.SARIMAX(train_set,order=param,                                 seasonal_order=param_seasonal,                                    enforce_stationarity=False,                                          enforce_invertibility=False)#append all the parameters and result AIC valueresults = mod.fit()        params.append(param)        seasonal.append(param_seasonal)        aic.append(results.aic)parameter_options=pd.DataFrame({'params':params,'seasonal_params':seasonal,'AIC':aic})#sort the AIC value to find the best fitted modelparameter_options.sort_values(by='AIC')
From the result above, we can see the top few performing parameter sets. However, this does not necessarily equal the best sets of parameters for prediction because out-of-sample accuracy could be very different from in-sample accuracy. You can refer to this list and do some trial and error to find the best fit model.
data_updated=data.set_index('date')train=data[data['date']<'2021-01-01']prediction=[]my_order = (0, 1, 1)my_seasonal_order = (1, 0, 1, 4)initial=len(train)initial_train=data_updated.iloc[:initial]model = sm.tsa.statespace.SARIMAX(initial_train['value'], order=my_order, seasonal_order=my_seasonal_order)results=model.fit()#Iteratively update the training data and predict following weekfor i in range(initial,len(data)):    updated_data=data_updated.iloc[i:i+1]['value']    results=results.append(updated_data,refit=False)    prediction.append(results.forecast()[0])from sklearn.metrics import mean_absolute_errorprint(mean_absolute_error(test['value'], prediction))#8469.6746
The model performs much better than two moving average results with a 15% reduction in Mean Average Error value.
Method 4 uses the same ARIMA model but with a different package that provides auto-tuning function. I am using the pm.auto_arima package and you can read more on the package functionality from the link.
Same as the previous method, you need to determine the value range of all the parameters. You can also specify whether to use stepwise or grid-search in your stepwise parameter. If you do not set stepwise=True, it would use grid-search at default which may be slow.
train_index=train.set_index('date')#Spesify the value range for parametersmodel = pm.auto_arima(train_index['value'],                           start_p=0, start_q=0,d=1, max_p=5,                           max_q=5, start_P=0, D=None, start_Q=0, max_P=5,                           max_D=5, max_Q=5,stepwise=True,seasonal=True)prediction=[]#Recurrently predict following week and add the data into training model once we have predicted that weekfor i in test['value']:    predict=model.predict(n_periods=1)[0]    prediction.append(predict)    model.update(i)from sklearn.metrics import mean_absolute_errorprint(mean_absolute_error(test['value'], prediction))#8863.74
The model has an update function that allows me to recurrently calculate the following week's data using historical data which is dynamic. Interestingly the final result has a slightly higher error number than the Manual Tuning method, this could be due to the stepwise method or slightly different algorithm between different packages.
When I plot the prediction of both SARIMA methods, I can tell these two follow more closely with the real step count trend than simple or exponential weighted moving average methods. However, SARIMA models also cannot capture the fluctuation very accurately. The error rate range between 20–30%.
In this example, I calculate recurrently for the following one-week value. However, it is very likely you need to predict the next N(N>1) weeks' value using historical data. It is much simpler actually because you can specify n_periods to the number of time_periods that you want to predict once you get the model(do not need to iteratively update the model)There are articles online on how you can determine the data is stationary or how we can determine the value of AR and MA terms using statistical methods like ‘Augmented Dickey Fuller test’. This can better determine the distribution of the data and prevent overfitting.SARIMA or ARIMA approach usually works better than simple moving average or exponentially weighted moving average approach because it considers the error termOne very important thing to take note of is that we should always remember to use data before the date that we predicting as the training data, it would be hard for us to use cross-validation.
In this example, I calculate recurrently for the following one-week value. However, it is very likely you need to predict the next N(N>1) weeks' value using historical data. It is much simpler actually because you can specify n_periods to the number of time_periods that you want to predict once you get the model(do not need to iteratively update the model)
There are articles online on how you can determine the data is stationary or how we can determine the value of AR and MA terms using statistical methods like ‘Augmented Dickey Fuller test’. This can better determine the distribution of the data and prevent overfitting.
SARIMA or ARIMA approach usually works better than simple moving average or exponentially weighted moving average approach because it considers the error term
One very important thing to take note of is that we should always remember to use data before the date that we predicting as the training data, it would be hard for us to use cross-validation.
Lastly, the final conclusion: It is really difficult to predict the step count at high accuracy level!
https://www.digitalocean.com/community/tutorials/a-guide-to-time-series-forecasting-with-arima-in-python-3 (how to select ARIMA parameters using lowest AIC)https://www.machinelearningplus.com/time-series/arima-model-time-series-forecasting-python/ (how to determine the parameter value statistically)https://github.com/markwk/qs_ledger/blob/master/apple_health/apple_health_extractor.ipynb. (code for extracting data from Apple Health)https://towardsdatascience.com/what-are-the-best-metrics-to-evaluate-your-regression-model-418ca481755b (how to evaluate your regression model)https://www.statsmodels.org/dev/generated/statsmodels.tsa.statespace.sarimax.SARIMAX.html (SARIMA model package, manual tuning)https://alkaline-ml.com/pmdarima/modules/generated/pmdarima.arima.auto_arima.html (SARIMA model package, auto-tuning)
https://www.digitalocean.com/community/tutorials/a-guide-to-time-series-forecasting-with-arima-in-python-3 (how to select ARIMA parameters using lowest AIC)
https://www.machinelearningplus.com/time-series/arima-model-time-series-forecasting-python/ (how to determine the parameter value statistically)
https://github.com/markwk/qs_ledger/blob/master/apple_health/apple_health_extractor.ipynb. (code for extracting data from Apple Health)
https://towardsdatascience.com/what-are-the-best-metrics-to-evaluate-your-regression-model-418ca481755b (how to evaluate your regression model)
https://www.statsmodels.org/dev/generated/statsmodels.tsa.statespace.sarimax.SARIMAX.html (SARIMA model package, manual tuning)
https://alkaline-ml.com/pmdarima/modules/generated/pmdarima.arima.auto_arima.html (SARIMA model package, auto-tuning)
Thanks for reading! If you have any questions or any topics you want to understand more please put them in the comment box!