# Section - 7 Predictive Modeling

We finally have everything we need to start making predictive models now that the data has been cleaned and we have come up with a gameplan to understand the efficacy of the models.

## 7.1 Example Simple Model

We can start by making a simple **linear regression** model:

```
##
## Call:
## lm(formula = target_price_24h ~ ., data = cryptodata)
##
## Coefficients:
## (Intercept) symbolADA symbolAGIX symbolALICE
## 8415.177199950 -0.012644860 4.890719622 0.081714000
## symbolANKR symbolAPT symbolARDR symbolARNM
## -0.035528239 -3.019583685 -0.011494319 0.569101565
## symbolATOM symbolAVA symbolBAKE symbolBAL
## 0.871278306 0.008178306 -0.023084479 0.436434282
## symbolBAT symbolBCH symbolBNT symbolBRD
## -0.019455396 9.153140201 0.012896809 0.246117141
## symbolBSV symbolBTC symbolBTG symbolCHZ
## 2.731692786 1738.247950098 1.097520520 -0.029261174
## symbolDASH symbolDCR symbolDODO symbolDOT
## 4.304073875 1.541820738 -0.086797462 0.409782775
## symbolDYDX symbolELF symbolENJ symbolEOS
## 0.167674176 -0.019456558 -0.007741986 0.055475494
## symbolETH symbolETHW symbolFEI symbolFIDA
## 116.448623293 0.315167763 0.678834728 0.013318051
## symbolFTT symbolGAS symbolGLMR symbolGMT
## 0.073784757 -1.085106883 -0.007623011 -0.007171660
## symbolGMTT symbolHT symbolINJ symbolIOTA
## -0.149636199 0.362704134 2.373651306 -0.009406202
## symbolKLAY symbolKMD symbolKNC symbolLAZIO
## -0.016006065 -0.019699533 0.016262817 0.344350520
## symbolLSK symbolLTC symbolMANA symbolMTLX
## 0.011144313 6.409294840 0.007007421 0.508542606
## symbolMXC symbolNEAR symbolNEXO symbolOXT
## -0.034315662 0.117101869 0.013269027 -2.501887123
## symbolPERP symbolPLI symbolQTUM symbolREN
## 0.363753037 -0.081830203 0.188348187 -0.058555442
## symbolRIF symbolRUNE symbolSAND symbolSKL
## -0.022971395 0.073131898 0.009861288 1.104052261
## symbolSNX symbolSTORJ symbolSXP symbolT
## 0.149178449 -0.011018027 -0.047144101 -0.059263581
## symbolTHETA symbolTOMO symbolTOWN symbolTRX
## 0.038296785 1.116817332 -0.053765132 -0.030028185
## symbolVGX symbolVIB symbolWAXP symbolXCH
## 0.030097783 -0.062336204 -0.161624467 2.706914845
## symbolXDC symbolXEM symbolXMR symbolXRD
## 0.014171315 -0.035489654 11.110995391 2.122482714
## symbolZEC symbolZRX date_time_utc date
## 2.837011736 -0.004280325 -0.000001902 -0.270215052
## price_usd lagged_price_1h lagged_price_2h lagged_price_3h
## 1.223506435 -0.071722649 -0.066828946 -0.006141156
## lagged_price_6h lagged_price_12h lagged_price_24h lagged_price_3d
## 0.026942794 -0.183236699 0.071799511 -0.064860579
## trainingtest trainingtrain split
## 1.566276256 -3.769233232 7.252883615
```

We defined the **formula** for the model as ** target_price_24h ~ .**, which means that we are want to make predictions for the

**target_price_24h**field, and use (

**) every other column found in the data (**

`~`

**). In other words, we specified a model that uses the**

`.`

**target_price_24h**field as the dependent variable, and all other columns (

**) as the independent variables. Meaning, we are looking to predict the**

`.`

**target_price_24h**, which is the only column that refers to the future, and use all the information available at the time the rest of the data was collected in order to infer statistical relationships that can help us forecast the future values of the

**target_price_24h**field when it is still unknown on new data that we want to make new predictions for.

In the example above we used the **cryptodata** object which contained all the non-nested data, and was a big oversimplification of the process we will actually use.

### 7.1.1 Using Functional Programming

From this point forward, we will deal with the new dataset **cryptodata_nested**, review the previous section where it was created if you missed it. Here is a preview of the data again:

```
## # A tibble: 390 x 5
## # Groups: symbol, split [390]
## symbol split train_data test_data holdout_data
## <chr> <dbl> <list> <list> <list>
## 1 BTC 1 <tibble [195 x 11]> <tibble [67 x 11]> <tibble [70 x 11]>
## 2 ETH 1 <tibble [200 x 11]> <tibble [68 x 11]> <tibble [69 x 11]>
## 3 EOS 1 <tibble [197 x 11]> <tibble [68 x 11]> <tibble [72 x 11]>
## 4 LTC 1 <tibble [200 x 11]> <tibble [68 x 11]> <tibble [68 x 11]>
## 5 BSV 1 <tibble [200 x 11]> <tibble [68 x 11]> <tibble [69 x 11]>
## 6 ADA 1 <tibble [200 x 11]> <tibble [68 x 11]> <tibble [69 x 11]>
## 7 ZEC 1 <tibble [200 x 11]> <tibble [68 x 11]> <tibble [69 x 11]>
## 8 HT 1 <tibble [197 x 11]> <tibble [68 x 11]> <tibble [69 x 11]>
## 9 TRX 1 <tibble [200 x 11]> <tibble [68 x 11]> <tibble [68 x 11]>
## 10 XMR 1 <tibble [138 x 11]> <tibble [66 x 11]> <tibble [69 x 11]>
## # ... with 380 more rows
```

Because we are now dealing with a **nested dataframe**, performing operations on the individual nested datasets is not as straightforward. We could extract the individual elements out of the data using **indexing**, for example we can return the first element of the column **train_data** by running this code:

```
## # A tibble: 195 x 11
## date_time_utc date price_usd target_price_24h lagged_price_1h
## <dttm> <date> <dbl> <dbl> <dbl>
## 1 2023-01-18 00:00:00 2023-01-18 21137. 20682. 21232.
## 2 2023-01-18 01:00:00 2023-01-18 21204. 20700. 21137.
## 3 2023-01-18 02:00:00 2023-01-18 21240. 20720. 21204.
## 4 2023-01-18 04:00:00 2023-01-18 21257. 20750. 21298.
## 5 2023-01-18 05:00:00 2023-01-18 21274. 20804. 21257.
## 6 2023-01-18 07:00:00 2023-01-18 21263. 20831. 21304.
## 7 2023-01-18 10:00:01 2023-01-18 21232. 20791. 21246.
## 8 2023-01-18 11:00:00 2023-01-18 21192. 20799. 21232.
## 9 2023-01-18 14:00:00 2023-01-18 21408. 20775. 21263.
## 10 2023-01-18 15:00:00 2023-01-18 21426. 20797. 21408.
## # ... with 185 more rows, and 6 more variables: lagged_price_2h <dbl>,
## # lagged_price_3h <dbl>, lagged_price_6h <dbl>, lagged_price_12h <dbl>,
## # lagged_price_24h <dbl>, lagged_price_3d <dbl>
```

remove STORJ to resolve weird problem that arose March 3rd, 2021:

As we already saw dataframes are really flexible as a data structure. We can create a new column in the data to store the models themselves that are associated with each row of the data. There are several ways that we could go about doing this (this tutorial itself was written to execute the same commands using three fundamentally different methodologies), but in this tutorial we will take a **functional programming** approach. This means we will focus the operations we will perform on the actions we want to take themselves, which can be contrasted to a **for loop** which emphasizes the objects more using a similar structure that we used in the example above showing the first element of the **train_data** column.

When using a **functional programming** approach, we first need to create functions for the operations we want to perform. Let’s wrap the **lm()** function we used as an example earlier and create a new custom function called **linear_model**, which takes a dataframe as an input (the **train_data** we will provide for each row of the nested dataset), and generates a linear regression model:

We can now use the **map()** function from the **purrr** package in conjunction with the **mutate()** function from **dplyr** to create a new column in the data which contains an individual linear regression model for each row of **train_data**:

```
## # A tibble: 385 x 6
## # Groups: symbol, split [385]
## symbol split train_data test_data holdout_data lm_model
## <chr> <dbl> <list> <list> <list> <list>
## 1 BTC 1 <tibble [195 x 11]> <tibble [67 x 11~ <tibble [70 x 11~ <lm>
## 2 ETH 1 <tibble [200 x 11]> <tibble [68 x 11~ <tibble [69 x 11~ <lm>
## 3 EOS 1 <tibble [197 x 11]> <tibble [68 x 11~ <tibble [72 x 11~ <lm>
## 4 LTC 1 <tibble [200 x 11]> <tibble [68 x 11~ <tibble [68 x 11~ <lm>
## 5 BSV 1 <tibble [200 x 11]> <tibble [68 x 11~ <tibble [69 x 11~ <lm>
## 6 ADA 1 <tibble [200 x 11]> <tibble [68 x 11~ <tibble [69 x 11~ <lm>
## 7 ZEC 1 <tibble [200 x 11]> <tibble [68 x 11~ <tibble [69 x 11~ <lm>
## 8 HT 1 <tibble [197 x 11]> <tibble [68 x 11~ <tibble [69 x 11~ <lm>
## 9 TRX 1 <tibble [200 x 11]> <tibble [68 x 11~ <tibble [68 x 11~ <lm>
## 10 XMR 1 <tibble [138 x 11]> <tibble [66 x 11~ <tibble [69 x 11~ <lm>
## # ... with 375 more rows
```

Awesome! Now we can use the same tools we learned in the high-level version to make a wider variety of predictive models to test

## 7.2 Caret

Refer back to the high-level version of the tutorial for an explanation of the caret package, or consult this document: https://topepo.github.io/caret/index.html

### 7.2.1 Parallel Processing

R is a ** single thredded** application, meaning it only uses one CPU at a time when performing operations. The step below is optional and uses the

**parallel**and

**doParallel**packages to allow R to use more than a single CPU when creating the predictive models, which will speed up the process considerably:

### 7.2.2 More Functional Programming

Now we can repeat the process we used earlier to create a column with the linear regression models to create **the exact same models**, but this time using the **caret** package.

```
linear_model_caret <- function(df){
train(target_price_24h ~ . -date_time_utc -date, data = df,
method = 'lm',
trControl=trainControl(method="none"))
}
```

*We specified the method as *

`lm`

for linear regression. See the high-level version for a refresher on how to use different methods to make different models: https://cryptocurrencyresearch.org/high-level/#/method-options. the trControl argument tells the caret package to avoid additional resampling of the data. As a default behavior caret will do re-sampling on the data and do hyperparameter tuning to select values to use for the paramters to get the best results, but we will avoid this discussion for this tutorial. See the official caret documentation for more details.Here is the full list of models that we can make using the **caret** package and the steps described the high-level version of the tutorial:

We can now use the new function we created **linear_model_caret** in conjunction with **map()** and **mutate()** to create a new column in the **cryptodata_nested** dataset called **lm_model** with the trained linear regression model for each split of the data (by cryptocurrency **symbol** and **split**):

We can see the new column called **lm_model** with the nested dataframe grouping variables:

```
## # A tibble: 385 x 3
## # Groups: symbol, split [385]
## symbol split lm_model
## <chr> <dbl> <list>
## 1 BTC 1 <train>
## 2 ETH 1 <train>
## 3 EOS 1 <train>
## 4 LTC 1 <train>
## 5 BSV 1 <train>
## 6 ADA 1 <train>
## 7 ZEC 1 <train>
## 8 HT 1 <train>
## 9 TRX 1 <train>
## 10 XMR 1 <train>
## # ... with 375 more rows
```

And we can view the summarized contents of the first trained model:

```
## Linear Regression
##
## 195 samples
## 10 predictor
##
## No pre-processing
## Resampling: None
```

### 7.2.3 Generalize the Function

We can adapt the function we built earlier for the linear regression models using caret, and add a parameter that allows us to specify the **method** we want to use (as in what predictive model):

### 7.2.4 XGBoost Models

Now we can do the same thing we did earlier for the linear regression models, but use the new function called **model_caret** using the **map2()** function to also specify the model as **xgbLinear** to create an **XGBoost** model:

```
cryptodata_nested <- mutate(cryptodata_nested,
xgb_model = map2(train_data, "xgbLinear", model_caret))
```

We won’t dive into the specifics of each individual model as the correct one to use may depend on a lot of factors and that is a discussion outside the scope of this tutorial. We chose to use the **XGBoost** model as an example because it has recently gained a lot of popularity as a very effective framework for a variety of problems, and is an essential model for any data scientist to have at their disposal.

There are several possible configurations for XGBoost models, you can find the official documentation here: https://xgboost.readthedocs.io/en/latest/parameter.html

### 7.2.5 Neural Network Models

We can keep adding models. As we saw, caret allows for the usage of over 200 predictive models. Let’s make another set of models, this time setting the **method** to to create

`dnn`

**deep neural networks**:

*Again, we will not dive into the specifics of the individual models, but a quick Google search will return a myriad of information on the subject.*

### 7.2.6 Random Forest Models

Next let’s use create Random Forest models using the **method** `ctree`

### 7.2.7 Principal Component Regression

For one last set of models, let’s make Principal Component Regression models using the **method** `pcr`

### 7.2.8 Caret Options

Caret offers some additional options to help pre-process the data as well. We outlined an example of this in the high-level version of the tutorial when showing how to make a **Support Vector Machine** model, which requires the data to be **centered** and **scaled** to avoid running into problems (which we won’t discuss further here).

## 7.3 Make Predictions

Awesome! We have trained the predictive models, and we want to start getting a better understanding of how accurate the models are on data they have never seen before. In order to make these comparisons, we will want to make predictions on the test and holdout datasets, and compare those predictions to what actually ended up happening.

In order to make predictions, we can use the **prediict()** function, here is an example on the first elements of the nested dataframe:

```
predict(object = cryptodata_nested$lm_model[[1]],
newdata = cryptodata_nested$test_data[[1]],
na.action = na.pass)
```

```
## 1 2 3 4 5 6 7 8
## 22927.16 22994.75 23019.95 23126.05 23146.67 23041.80 22964.32 23004.27
## 9 10 11 12 13 14 15 16
## 23028.90 23063.33 23088.79 23144.86 23088.52 23029.50 23014.75 22940.07
## 17 18 19 20 21 22 23 24
## 22717.61 22808.98 22867.46 22921.45 23110.62 23104.50 23141.06 23013.63
## 25 26 27 28 29 30 31 32
## 22969.27 22979.90 23001.87 22991.02 22971.07 23022.48 23018.58 23171.26
## 33 34 35 36 37 38 39 40
## 23226.50 23216.75 23233.33 23116.29 23068.23 22982.42 23036.85 23125.73
## 41 42 43 44 45 46 47 48
## 23083.00 23056.42 23091.51 23061.37 23023.30 22999.64 22954.59 22955.83
## 49 50 51 52 53 54 55 56
## 22977.43 22975.39 22967.24 22954.50 22964.40 22932.51 22998.85 23011.97
## 57 58 59 60 61 62 63 64
## 23005.25 22997.86 23032.57 23005.50 23092.45 23010.67 23068.26 23166.50
## 65 66 67
## 23269.46 23199.00 23193.38
```

Now we can create a new custom function called **make_predictions** that wraps this functionality in a way that we can use with **map()** to iterate through all options of the nested dataframe:

```
make_predictions <- function(model, test){
predict(object = model, newdata = test, na.action = na.pass)
}
```

Now we can create the new columns **lm_test_predictions** and **lm_holdout_predictions** with the predictions:

```
cryptodata_nested <- mutate(cryptodata_nested,
lm_test_predictions = map2(lm_model,
test_data,
make_predictions),
lm_holdout_predictions = map2(lm_model,
holdout_data,
make_predictions))
```

The predictions were made using the models that had only seen the **training data**, and we can start assessing how good the model is on data it has not seen before in the **test** and **holdout** sets. Let’s view the results from the previous step:

```
## # A tibble: 385 x 4
## # Groups: symbol, split [385]
## symbol split lm_test_predictions lm_holdout_predictions
## <chr> <dbl> <list> <list>
## 1 BTC 1 <dbl [67]> <dbl [70]>
## 2 ETH 1 <dbl [68]> <dbl [69]>
## 3 EOS 1 <dbl [68]> <dbl [72]>
## 4 LTC 1 <dbl [68]> <dbl [68]>
## 5 BSV 1 <dbl [68]> <dbl [69]>
## 6 ADA 1 <dbl [68]> <dbl [69]>
## 7 ZEC 1 <dbl [68]> <dbl [69]>
## 8 HT 1 <dbl [68]> <dbl [69]>
## 9 TRX 1 <dbl [68]> <dbl [68]>
## 10 XMR 1 <dbl [66]> <dbl [69]>
## # ... with 375 more rows
```

Now we can do the same for the rest of the models:

```
cryptodata_nested <- mutate(cryptodata_nested,
# XGBoost:
xgb_test_predictions = map2(xgb_model,
test_data,
make_predictions),
# holdout
xgb_holdout_predictions = map2(xgb_model,
holdout_data,
make_predictions),
# Neural Network:
nnet_test_predictions = map2(nnet_model,
test_data,
make_predictions),
# holdout
nnet_holdout_predictions = map2(nnet_model,
holdout_data,
make_predictions),
# Random Forest:
rf_test_predictions = map2(rf_model,
test_data,
make_predictions),
# holdout
rf_holdout_predictions = map2(rf_model,
holdout_data,
make_predictions),
# PCR:
pcr_test_predictions = map2(pcr_model,
test_data,
make_predictions),
# holdout
pcr_holdout_predictions = map2(pcr_model,
holdout_data,
make_predictions))
```

We are done using the **caret** package and can stop the parallel processing cluster:

## 7.4 Timeseries

Because this tutorial is already very dense, we will just focus on the models we created above. When creating predictive models on timeseries data there are some other excellent options which consider when the information was collected in similar but more intricate ways to the way we did when creating the lagged variables.

For more information on using excellent tools for ARIMA and ETS models, consult the high-level version of this tutorial where they were discussed.

Move on to the next section ➡️ to assess the accuracy of the models as described in the previous section.