The Tidy Finance books use SQLite for data storage. However, SQLite appears to have appalling performance for some common tasks. In this note, I discuss some options for dramatically improving on this performance (e.g., reducing time to read data off disk from over two minutes to under one second).
Keywords
Python
1 Introduction
Tidy Finance with R and Tidy Finance with Python provide excellent introductions to doing data analysis for academic finance. Chapters 2–4 of either book provide code to establish an SQLite database that is used as the data source for the rest of the book. Recently I have been dabbling with the Python version of the book and have found the analyses to be surprisingly sluggish on my computer. This note explores some options for improving this performance while still getting the results found in the book.1
Here I show how one can easily convert an SQLite database to a set of parquet files. A similar approach could be used to convert, say, a schema of a PostgreSQL database to parquet files.2 I may explore such an approach as an alternative to the wrds2pg package, which runs SAS code on the WRDS server to generate data used to construct parquet files.3
A virtue of the approach used in Tidy Finance is that it is flexible. Readers of Tidy Finance can easily incorporate the approach used here as they work though the core parts of that book. The source code for this note can be found here.
In this note, I will load the following R packages. I will also use duckdb and RSQLite packages. use install.packages() to get any package that you don’t already have.
To start with, I follow the approach used in Tidy Finance, which generally involves connecting to the SQLite database before using collect() to read data into memory in R. The following code is pretty much copy-pasted from Chapter 5.
These numbers indicate mind-bogglingly bad performance. Mind-boggling because it is not clear how it can take so long to read 4GB of structured data into memory.
3 Using DuckDB to interact with SQLite
Rather than using RSQLite to interact with the Tidy Finance SQLite database, we could use DuckDB. DuckDB is touted as “SQLite for analytics”, as it does not require a separate server process and is easy to install, but has a much more feature-rich SQL dialect and a larger set of data types and offers superior performance. To use DuckDB, we first instantiate an in-memory database.
db <- dbConnect(duckdb::duckdb())
Next, we load the sqlite extension and attach the Tidy Finance SQLite database. Note that the INSTALL takes less than a second and happens only once per installation of DuckDB, but the LOAD is needed for each new connection.6 Windows users may need to do more work to install the sqlite extension (see discussion here).7
While much faster, more than ten seconds is not great. In fact, we may find it better to leave the data in the database and do computations there. For example, we can do a version of the monthly beta calculations done in Tidy Finance inside the DuckDB database. This calculation requires two tables: crsp_monthly and factors_ff3_monthly.
We have loaded data, calculated r prettyNum(nrow(beta_monthly), big.mark = ",") betas (each involving a regression) and brought the data into memory in R in about half a second!
beta_monthly
4 Converting the Tidy Finance database to parquet files
Rather than keeping the data in an SQLite database, we could actually convert the entire Tidy Finance database to parquet files using the following function, which leverages DuckDB’s ability to create parquet files.9
Converting all the tables takes around 20 seconds. While there are now 11 files rather than one, these files use less than a quarter of the disk space of the original database file.
Now that we have the everything in parquet format, we can read the data even faster than using DuckDB to read SQLite tables. These files can be used equally easily with Python. And R and Python have libraries (e.g., arrow in R) for working with parquet files directly.
Returning to our original benchmark, how long does it take to load the crsp_daily data into memory in R?
Let’s do the beta calculation again. Everything is as it was above except the first two lines, which point crsp_monthly and factors_ff3_monthly to parquet files in place of SQLite tables. Perhaps unsurprisingly, performance is similar—it would be difficult to beat what we saw earlier by much.
I ran the code in this note on an M1 Pro MacBook Pro. Running the same code on an i7-3770K running Linux Mint gave similar relative results. Fast code on an old computer handily beats slow code on a new computer.↩︎
This would involve using the postgres extension to DuckDB.↩︎
Issue to be addressed would be supporting the various arguments to the wrds_update() function in wrds2pg, such as col_types and keep, and addressing memory limitations with large tables.↩︎
Unlike the base R system.time(), this function works with assignment. If we put system.time() at the end of a pipe, then the value returned by system.time() would be stored rather than the result of the pipeline preceding it. Hadley Wickham explained to me that this function works because of lazy evaluation, which is discussed in “Advanced R” here. Essentially, x is evaluated just once—inside system.time()—and its value is returned in the next line.↩︎
Chapter 4 covers data that are not needed in the later chapters I am currently looking at.↩︎
Subsequent calls to INSTALL appear to have no effect.↩︎
I don’t have ready access to a Windows computer to check.↩︎
Usually one would use group_by(), window_order(), and window_range() to generate the requisite PARTITION BY, ORDER BY, and ROWS BETWEEN SQL clauses. However, a gap in the dbplyr package means that it doesn’t recognize regr_slope() and regr_count() functions as aggregates that require them to be put in a “window context”. I will likely file an issue related to this on the dbplyr GitHub page soon.↩︎
Make sure you don’t use this function with a database that already has a table named df in it, especially if you care about that table.↩︎