<jupyter_start><jupyter_text>Module 8 Video 37: Predictive Modelling II**Python for the Energy Industry**In the previous lesson, we fit a first order autoregressive model onto floating storage data. Here, we will discuss finding the most appropriate type of ARIMA model for fitting to a time series. Start by getting the data:<jupyter_code># initial imports
import pandas as pd
import numpy as np
from datetime import datetime
from dateutil.relativedelta import relativedelta
import vortexasdk as v
from statsmodels.tsa.arima.model import ARIMA

# The cargo unit for the time series (barrels)
TS_UNIT = 'b'

# The granularity of the time series
TS_FREQ = 'day'

# datetimes to access last 7 weeks of data
now = datetime.utcnow()
seven_weeks_ago = now - relativedelta(weeks=7)
# Find crude ID
crude = [p.id for p in v.Products().search('crude').to_list() if p.name=='Crude']
assert len(crude) == 1

search_result = v.CargoTimeSeries().search(
    timeseries_frequency=TS_FREQ,
    timeseries_unit=TS_UNIT,
    filter_products=crude,
    filter_time_min=seven_weeks_ago,
    filter_time_max=now,
    # Filter for cargo in floating storage
    filter_activity="storing_state",
    # Only get floating storage that lasted longer than 21 days
    timeseries_activity_time_span_min=1000 * 60 * 60 * 24 * 14,
)

df_floating_storage = search_result.to_df()
df_floating_storage = df_floating_storage.rename(columns = {'key': 'date', 'value': 'quantity'})[['date','quantity']]<jupyter_output><empty_output><jupyter_text>An important question to ask about time series data before fitting an ARIMA model is whether the data is [stationary](https://en.wikipedia.org/wiki/Stationary_process). Non-stationary data should be differenced, using an ARIMA(p,d,q) model with d > 0. We can test stationarity with the Augmented Dickey Fuller test:<jupyter_code>from statsmodels.tsa.stattools import adfuller
adfresult = adfuller(df_floating_storage['quantity'])
print('ADF Statistic: ', adfresult[0])
print('p-value: ', adfresult[1])<jupyter_output>ADF Statistic:  -1.1815462355916246
p-value:  0.6814832570453857<jupyter_text>So, we cannot reject the null hypothesis that the time series is non-stationary. What about the first order difference of the time series:<jupyter_code>adfresult = adfuller(df_floating_storage['quantity'].diff().dropna())
print('ADF Statistic: ', adfresult[0])
print('p-value: ', adfresult[1])<jupyter_output>ADF Statistic:  -7.164255346445803
p-value:  2.914087126669944e-10<jupyter_text>So it seems that we would only need 1 order of differencing, i.e. d=1. It's also useful to look at the autocorrelation and partial autocorrelation (where the contribution of intervening lags is removed). We will do this using the below functions from the statsmodels module, for both the original and 1-order-differenced data:<jupyter_code>from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
ax1 = plot_acf(df_floating_storage['quantity'])
ax2 = plot_pacf(df_floating_storage['quantity'])
ax1 = plot_acf(df_floating_storage['quantity'].diff().dropna())
ax2 = plot_pacf(df_floating_storage['quantity'].diff().dropna())<jupyter_output><empty_output><jupyter_text>From these plots, we can see that there is no autocorrelation in the differenced data, so if d=1 then we should use an ARIMA(0,1,0). In the original data, there is autocorrelation which is fully accounted by 1 day lag, so if using d=0 as in the previous lesson then ARIMA(1,0,0) should be optimal. Let's try an ARIMA(0,1,0) model, and see how it compares to our ARIMA(1,0,0) model:<jupyter_code>arima = ARIMA(df_floating_storage['quantity'].head(42),order=(0,1,0))
res = arima.fit()

pred = res.get_prediction(start=1,end=49)
df_floating_storage['prediction'] = pred.predicted_mean
df_floating_storage['lower'] = pred.conf_int()['lower quantity']
df_floating_storage['upper'] = pred.conf_int()['upper quantity']

ax = df_floating_storage.plot(x='date',y=['quantity','prediction'])
ax.fill_between(df_floating_storage['date'].values,df_floating_storage['lower'],df_floating_storage['upper'],color='g',alpha=.1)<jupyter_output><empty_output><jupyter_text>This models performs similarly to the ARIMA(1,0,0) model. Notably, since an ARIMA(0,1,0) model is equivalent to a random walk, the out-of-sample predicted mean is a constant value. Finally, we can also assess different model types numerically, testing a range of different models. There are a range of metrics we can use to measure model performance, here we will use mean-square-error (MSE) and the Akaike information criterion (AIC), whiuch penalises higher order models.Doing a loop over different sets of (p,d,q):<jupyter_code>MSEs = {}
AICs = {}

for p in range(4):
    for d in range(2):
        for q in range(4):
            arima = ARIMA(df_floating_storage['quantity'].head(42),order=(p,d,q))
            res = arima.fit()
            mse = res.mse
            aic = res.aic
            MSEs[(p,d,q)] = mse
            AICs[(p,d,q)] = aic
results = pd.DataFrame({'MSE':MSEs.values(),'AIC':AICs.values()},index=MSEs.keys())
results.head()
results[results['MSE'] < 1e13]<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10905189231369489, 'fraction_numerical': 0.026921576277799453, 'mean_word_length': 5.710732984293194, 'pattern_counts': {'":': 0, '<': 24, '<?xml version=': 0, '>': 24, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '286475', 'license': 'Apache-2.0', 'n_tokens_mistral': 1808, 'n_tokens_neox': 1672, 'n_words': 497, 'path': 'docs/examples/academy/37. Predictive Modelling II.ipynb', 'repo_name': 'V0RT3X4/python-sdk', 'size': '5126'}