Correction For Spatial And Temporal Auto-Correlation In Panel Data: Using R To Estimate Spatial HAC Errors Per Conley

tl;dr: Fast computation of standard errors that allows for serial and spatial auto-correlation.

Economists and political scientists often employ panel data that track units (e.g., firms or villages) over time. When estimating regression models using such data, we often need to be concerned about two forms of auto-correlation: serial (within units over time) and spatial (across nearby units). As Cameron and Miller (2013) note in their excellent guide to cluster-robust inference, failure to account for such dependence can lead to incorrect conclusions: “[f]ailure to control for within-cluster error correlation can lead to very misleadingly small standard errors…” (p. 4).

Conley (1999, 2008) develops one commonly employed solution. His approach allows for serial correlation over all (or a specified number of) time periods, as well as spatial correlation among units that fall within a certain distance of each other. For example, we can account for correlated disturbances within a particular village over time, as well as between that village and every other village within one hundred kilometers. As with serial correlation, spatial correlation can be positive or negative. It can be made visually obvious by plotting, for example, residuals after removing location fixed effects.

Example Visualization of Spatial Correlation from Radil, S. Matthew, Spatializing Social Networks: Making Space for Theory In Spatial Analysis, 2011.

We provide a new function that allows R users to more easily estimate these corrected standard errors. (Solomon Hsiang (2010) provides code for STATA, which we used to test our estimates and benchmark speed.) Moreover using the excellent lfe, Rcpp, and RcppArmadillo packages (and Tony Fischetti’s Haversine distance function), our function is roughly 20 times faster than the STATA equivalent and can scale to handle panels with more units. (We have used it on panel data with over 100,000 units observed over 6 years.)

This demonstration employs data from Fetzer (2014), who uses a panel of U.S. counties from 1999-2012. The data and code can be downloaded here.

STATA Code:

We first use Hsiang’s STATA code to compute the corrected standard errors (spatHAC in the output below). This routine takes just over 25 seconds.

cd "~/Dropbox/ConleySEs/Data"
clear; use "new_testspatial.dta"

tab year, gen(yy_)
tab FIPS, gen(FIPS_)

timer on 1
ols_spatial_HAC EmpClean00 HDD yy_*FIPS_2-FIPS_362, lat(lat ) lon(lon ) t(year) p(FIPS) dist(500) lag(5) bartlett disp

# *-----------------------------------------------
# *    Variable |   OLS      spatial    spatHAC
# *-------------+---------------------------------
# *         HDD |   -0.669     -0.669     -0.669
# *             |    0.608      0.786      0.838

timer off 1
timer list 1
#  1:     24.8 /        3 =      8.2650

R Code:

Using the same data and options as the STATA code, we then estimate the adjusted standard errors using our new R function. This requires us to first estimate our regression model using the felm function from the lfe package.

# Loading sample data:
dta_file <- "~/Dropbox/ConleySEs/Data/new_testspatial.dta"
setnames(DTA, c("latitude", "longitude"), c("lat", "lon"))

source("~/Dropbox/ConleySEs/ConleySEs_17June2015.R")

ptm <-proc.time()

# We use the felm() from the lfe package to estimate model with year and county fixed effects.
# Two important points:
# (1) We specify our latitude and longitude coordinates as the cluster variables, so that they are included in the output (m).
# (2) We specify keepCx = TRUE, so that the centered data is included in the output (m).

m <-felm(EmpClean00 ~HDD -1 |year +FIPS |0 |lat +lon,
data = DTA[!is.na(EmpClean00)], keepCX = TRUE)

coefficients(m) %>%round(3) # Same as the STATA result.
   HDD
-0.669 

We then feed this model to our function, as well as the cross-sectional unit (county FIPS codes), time unit (year), geo-coordinates (lat and lon), the cutoff for serial correlation (5 years), the cutoff for spatial correlation (500 km), and the number of cores to use.

SE <-ConleySEs(reg = m,
unit = "FIPS",
time = "year",
lat = "lat", lon = "lon",
dist_fn = "SH", dist_cutoff = 500,
lag_cutoff = 5,
cores = 1,
verbose = FALSE)

sapply(SE, sqrt) %>%round(3) # Same as the STATA results.
        OLS     Spatial Spatial_HAC
0.608       0.786       0.837 
proc.time() -ptm
   user  system elapsed
1.619   0.055   1.844 

Estimating the model and computing the standard errors requires just over 1 second, making it over 20 times faster than the comparable STATA routine.

R Using Multiple Cores:

Even with a single core, we realize significant speed improvements. However, the gains are even more dramatic when we employ multiple cores. Using 4 cores, we can cut the estimation of the standard errors down to around 0.4 seconds. (These replications employ the Haversine distance formula, which is more time-consuming to compute.)

pkgs <-c("rbenchmark", "lineprof")
invisible(sapply(pkgs, require, character.only = TRUE))

bmark <-benchmark(replications = 25,
columns = c('replications','elapsed','relative'),
ConleySEs(reg = m,
unit = "FIPS", time = "year", lat = "lat", lon = "lon",
dist_fn = "Haversine", lag_cutoff = 5, cores = 1, verbose = FALSE),
ConleySEs(reg = m,
unit = "FIPS", time = "year", lat = "lat", lon = "lon",
dist_fn = "Haversine", lag_cutoff = 5, cores = 2, verbose = FALSE),
ConleySEs(reg = m,
unit = "FIPS", time = "year", lat = "lat", lon = "lon",
dist_fn = "Haversine", lag_cutoff = 5, cores = 4, verbose = FALSE))
bmark %>%mutate(avg_eplased = elapsed /replications, cores = c(1, 2, 4))
  replications elapsed relative avg_eplased cores
1           25   23.48    2.095      0.9390     1
2           25   15.62    1.394      0.6249     2
3           25   11.21    1.000      0.4483     4

Given the prevalence of panel data that exhibits both serial and spatial dependence, we hope this function will be a useful tool for applied econometricians working in R.

Feedback Appreciated: Memory vs. Speed Tradeoff

This was Darin’s first foray into C++, so we welcome feedback on how to improve the code. In particular, we would appreciate thoughts on how to overcome a memory vs. speed tradeoff we encountered. (You can email Darin at darinc[at]stanford.edu.)

The most computationally intensive chunk of our code computes the distance from each unit to every other unit. To cut down on the number of distance calculations, we can fill the upper triangle of the distance matrix and then copy it to the lower triangle. With $N$ units, this requires only  $(N (N-1) /2)$ distance calculations.

However, as the number of units grows, this distance matrix becomes too large to store in memory, especially when executing the code in parallel. (We tried to use a sparse matrix, but this was extremely slow to fill.) To overcome this memory issue, we can avoid constructing a distance matrix altogether. Instead, for each unit, we compute the vector of distances from that unit to every other unit. We then only need to store that vector in memory. While that cuts down on memory use, it requires us to make twice as many   $(N (N-1))$  distance calculations.

As the number of units grows, we are forced to perform more duplicate distance calculations to avoid memory constraints – an unfortunate tradeoff. (See the functions XeeXhC and XeeXhC_Lg in ConleySE.cpp.)

sessionInfo()
R version 3.2.2 (2015-08-14)
Platform: x86_64-apple-darwin13.4.0 (64-bit)
Running under: OS X 10.10.4 (Yosemite)

locale:
[1] en_GB.UTF-8/en_GB.UTF-8/en_GB.UTF-8/C/en_GB.UTF-8/en_GB.UTF-8

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods
[7] base

other attached packages:
[3] geosphere_1.4-3           sp_1.1-1
[5] lfe_2.3-1709              Matrix_1.2-2
[7] ggplot2_1.0.1             foreign_0.8-65
[9] data.table_1.9.4          dplyr_0.4.2
[11] knitr_1.11

loaded via a namespace (and not attached):
[1] Formula_1.2-1    magrittr_1.5     MASS_7.3-43
[4] munsell_0.4.2    xtable_1.7-4     lattice_0.20-33
[7] colorspace_1.2-6 R6_2.1.1         stringr_1.0.0
[10] plyr_1.8.3       tools_3.2.2      parallel_3.2.2
[13] grid_3.2.2       gtable_0.1.2     DBI_0.3.1
[16] htmltools_0.2.6  yaml_2.1.13      assertthat_0.1
[19] digest_0.6.8     reshape2_1.4.1   formatR_1.2
[22] evaluate_0.7.2   rmarkdown_0.8    stringi_0.5-5
[25] compiler_3.2.2   scales_0.2.5     chron_2.3-47
[28] proto_0.3-10    

Leveraging R for Econ Job Market

[UPDATE] I just was told that the new features on EJM actually allow you to download an XLS spreadsheet of the job listings on EJM. This is accessible when you login to myeconjobmarket.org and is part of their new AIMS (Application and Interview Management System).

I wanted to describe a little helper I am using to help refine the places I want to apply at since I am going to be on the Economics Job Market this year.

The two main websites were job openings are advertised are:

Now JOE has a really nice feature where you can download simply all job openings into a nice spreadsheet. This allows you to browse through and refine your search. Econ Job Market does not have such a feature.

The Listing page is quite annoying…

If you want more details for a job opening, such as a description of the fields and application requirements, you will have to click on “more info…”. In the JOE spreadsheet, you have all that information at once.

I wanted to create a JOE like spreadsheet using the openings from EJM. Some of which, of course, do overlap. But for some reason, some jobs are only on EJM but not on JOE.

So how can we use R to help us do that?

The first thing I wanted to do is get the simple listings on the main application page from EJM as a spreadsheet. You can simply download the main listings file and extract the pieces of information. Most important is the “posid” field, which is the position ID contained in the EJM URL. This will give you a direct link to the HTML page of the job opening and it also tells you whether you can apply through EJM.

This leaves you with a listing of EJM links to jobs, their position ID and the EJM Application Link in case the Job Opening accepts applications through EJM. Now you can proceed to simply download all the HTML files using a batch downloader such as DownThemAll. If you want to do that, you can print out a list:

('.sample1').ClassyCompare({ defaultgap:50, leftgap:0, rightgap:10, caption: true, reveal: 0.5 }); }); </script> </div> </section> </div> </section> </div> </div> </body> </html> This is how it looks — I know the stuff is not perfectly aligned, partly because when cropping the picture I made a mistake and could not be bothered with fixing it. Have fun! Computing Maritime Routes in R Thanks to the attention my paper on the cost of Somali piracy has received, a lot of people have approached me to ask how I computed the maritime routes. It is not a very difficult task using R. The key ingredient is a map of the world, that can be rasterized into a grid; all the landmass needs to be assigned an infinite cost of crossing and last but not least — one needs to compute the actual routes. What packages do I need? library(gdistance) library(maptools) data(wrld_simpl) library(data.table) The package gdistance does most of the actual work of computing the routes. The wrld_simpl map provides what is needed to generate a raster. Generating a Raster #create a raster from shape files shp <- wrld_simpl r <- raster() r <-rasterize(shp, r, progress='text') After the raster is generated, we can proceed by making landmass impassable for vessels. #make all sea = -999 r[is.na(r)] <- -999 #this turns all landmass to missing r[r>-999] <- NA #assign unit cost to all grid cells in water r[r==-999] <- 1 There are a few more things to do, such as opening up the Suez Canal and some other maritime passages — one needs to find the right grid cells for this task. In the next step we can transform the raster into a transition layer matrix, that comes from the gdistance package. It is a data construct that essentially tells us how one can move from one cell to the other — you can allow diagonal moves by allowing the vessel to move into all 8 adjacent grid cells. There is also a geo-correction necessary, as the diagonals are longer distances than the straight-line moves. tr <- transition(r, mean, directions = 8) tr <- geoCorrection(tr, "c") Well — and thats basically it — of course, there are a few bits and pieces that need additional work — like adding heterogenuous costs as one can imagine exist due to maritime currents and so on. Furthermore, there is a whole logic surrounding the handling of the output and the storing in a local database for further use and so on. But not to bore you with that — how can I obtain the distance between A and B? This uses Dijkstra’s Algorithm and is called through the gdistance function “shortestPath”. AtoB <- shortestPath(tr, as.numeric(start[1:2]), as.numeric(end[1:2]), output = "SpatialLines") Using this output, you can then generate fancy graphs such as … Starting Multiple Stata Instances on Mac I found it useful to have multiple Stata instances running on my Mac, in particular, if I use one instance to clean the data before running merge commands. It is always annoying if the merging does not work out or throws an error and then, one would have to clear the current instance and open the DTA file that was messing up the merge. Its a simple command that allows you to open multiple Stata instances on a Mac: Code:  1  open -n /Applications/Stata12_OSX/StataSE.app You can also define an alias command in your .bash_profile, Code:  1  alias stata='open -n /Applications/Stata12_OSX/StataSE.app' Good luck! R function: generate a panel data.table or data.frame to fill with data I have started to work with R and STATA together. I like running regressions in STATA, but I do graphs and setting up the dataset in R. R clearly has a strong comparative advantage here compared to STATA. I was writing a function that will give me a (balanced) panel-structure in R. It then simply works by joining in the additional data.tables or data.frames that you want to join into it. It consists of two functions: timeVector <- function(starttime,endtime,timestep="months") { starttime<- as.POSIXct(strptime(starttime, '%Y-%m-%d')) endtime<- as.POSIXct(strptime(endtime, '%Y-%m-%d')) if(timestep=="quarters") { timestep="months" ret<-seq(from=as.POSIXct(starttime), to=as.POSIXct(endtime), by=timestep) quarter <- gsub("(^[123]{1})", 1, month(ret))
quarter <- gsub("(^[456]{1}$)", 2, quarter) quarter <- gsub("(^[789]{1}$)", 3, quarter)
quarter <- as.numeric(gsub("(^[102]{2}$)", 4, quarter)) ret<-paste(year(ret),quarter,sep="-") ret<-unique(ret) } else { ret<-seq(from=as.POSIXct(starttime), to=as.POSIXct(endtime), by=timestep) } ret } This first function generates the time-vector, you need to tell it what time-steps you want it to have. panelStructure <- function(group,timevec) { tt<-rep(timevec,length(group)) tt2 <- as.character(sort(rep(group,length(timevec)))) mat <- cbind("group"=data.frame(tt2),"timevec"=data.frame(tt)) names(mat)<-c("group","timevec") mat } This second function then generates the panel-structure. You need to give it a group vector, such as for example a vector of district names and you need to pass it the time vector that the other function created. Hope this is helpful to some of you. Removing Multibyte Characters from Strings I was a bit annoyed by the error when loading a dataset that contains multi-byte characters. R basically just chokes on them. I have not really understood the intricacies of this, but it was basically just an annoyance and since I did not really use these characters in the strings containing them, I just wanted to remove them. The easiest solution was to use Vim with the following search and replace: s/[\x80-\xFF]//g Downloading All Your Pictures From iPad or iPhone I really disklike iTunes, it is the worst piece of software I have ever come accross. I would say that Windows has been getting better and better. I had the following problem: I uploaded quite a few pictures via iTunes onto my iPad, just because its nice to look at pictures on that machine. However, the machine with which I did the syncing broke and needed repair and somehow, I forgot to save these pictures onto a hard drive for backup. So the only place where these pictures now rest is on my iPad. iTunes wont allow you to copy pictures on your iPad onto a machine (only the pictures that you atually take with the iPad). This is because, these pictures *should* be on the machine with which you synced your iPad in the first place. However, this was not true in my case anymore. Now you could either invest some money and purchase an app that allows you to copy your picture albums from the iPad onto a Windows machine. There is e.g. CopyTrans Suite, which is a bit costly and in the version I tried, did not copy the full resolution of the pictures (which is a rip-off!). So I was looking into a cheap and quick solution to get the original full resolution pictures down from my iPad. Setting things up: installing free app “WiFi Photo” This app basically makes your photo albums available on a local webserver. Once you start the app on the iPad, it tells you an URL to brows to on your local machine. There you can see all the pictures that are on your iPad. You could now use this app to manually download the pictures, however, it is limited to 100 pictures at once and you will not get the full resolution pictures if you do a batch download. If you browse through the app, you will notice that the URL to the full resolution pictures has the following form: http://192.168.1.6:15555/0/fr_564.jpg where the “0″ stands for the album ID. If you have, say 2 albums on the iPad, this would take values “0″ or “1″. Images are stored as consecutive numbers in each album, so the following link would go to picture number 564 in full resolution in album 0. So we will exploit this structure to do an automated batch download. Doing an automated batch download First, in order for this to work you need to get a a local PHP installation up and running. If you are really lazy, you could just install XAMPP. However, you can implement the code in any other coding language, e.g. in R as well. To download all the pictures, you need to adjust and run the following script Code:  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20  for($k=0;$k<=3;$k++) {   for($i=1;$i<=1000;$i++) { //adjust this$url = "http://192.168.1.9:15555/".$k."/fr_".$i.".jpg";   //adjust this $fn = "C:/Dokumente und Einstellungen/Thiemo/Desktop/Kolumbien/".$k."-".$i.".jpg"; //to make sure you dont redownload a file already downloaded if you want //to run the script several times if(!file_exists($fn)) { if($content = file_get_contents($url)) { $fp = fopen($fn,"a+"); fwrite($fp,$content); fclose(\$fp); } } }
What this script does it iterates through the albums (the first loop), in my case I have four albums. The second loop then iterates through the pictures, I simply assume that there are at most 1000 pictures in each album. Clearly, this can be made smarter, i.e. automatically find out how many pictures in each album, but this works and thats all we need.
I would recommend running the script a few times, as sometimes it is not able to retrieve the content and then, no file is created. By adding the “file_exists” check, I make sure that no picture, that has been downloaded already, is downloaded again. So if you run the script several times, it will be quicker and quicker to also pick up the last missing pictures.
Running the script takes some time as it needs to copy down each picture, and in my case this were a rough 2000 pictures. But now, they are back in the safe haven of my local machine.

Microfinance in India: Getting a sense of the geographic distribution

I am working on a review paper on microfinance in India and use data from the MIX market. Today, I was amazed by how quick I conjured a map of India with the headquarters of the microfinance institutions that report data to the MIX market depicted on that map. Ideally, I would have more geolocation data – but this is hard to come by. But what we can clearly see is the clustering of institutions in big cities and in the south, which was hit hardest by the recent crisis.

Microfinance Institutions across India

I dont think anybody has produced such a map before. In fact, I can do this for all institutions reporting data around the world, which may be interesting to see. Also, I already tried to make the size of the dot proportional to e.g. measures of real yield or color-coding the nearest neighborhood (say the neigbhouring districts) by the average loan sizes reported. Lots of things to do. Maybe thats something for the guys at MIX Market or for David Roodman who, I think has finished his open book.

The key difficulty was actually not in plotting the map (though it took some time), but in obtaining geo-data on where the headquarters of the microfinance institutions are located. I managed to obtain this data – though its not perfect – by making calls to the Google MAP API via a PHP script., basically using the following two functions:

R Function Binding Vectors and Matrices of Variable Length, bug fixed

Now this is something very geeky, but useful. I had to bind two matrices or vectors together to become a bigger matrix. However, they need not have the same number of rows or even the same row names.

The standard cbind() functions require the vectors or matrices to be compatible. The matching is “stupid”, in the sense that it ignores any order or assumes that the elements which are to be joined into a matrix have the same row names. This, of course, need not be the case. A classical merge command would fail here, as we dont really know what to merge by and what to merge on.

Ok… I am not being clear here. Suppose you want to merge two vectors

Code:
 1 2 3  A 2 B 4 C 3

and

Code:
 1 2 3 4  G 2 B 1 C 3 E 1

now the resulting matrix should be

Code:
 1 2 3 4 5  A 2 NA B 4 1 C 3 3 E NA 1 G NA 2

Now the following Rfunction allows you to do this. It is important however, that you assign rownames to the objects to be merged (the A,B,C,E,G in the example), as it does matching on these.

cbindM <-
function(A, v, repl=NA) {

dif <- setdiff(union(rownames(A),rownames(v)),intersect(rownames(A),rownames(v)))
#if names is the same, then a simple cbind will do
if(length(dif)==0) {

A<- cbind(A,v[match(rownames(A),rownames(v))])

rownames(A) <- rownames(v)

}    else if(length(dif)>0) {#sets are not equal, so either matrix is longer / shorter

#this tells us which elements in dif are part of A (and of v) respectively
for(i in dif)     {

if(is.element(i,rownames(A))) {
#element is in A but not in v, so add it to v and then a

temp<-matrix(data =repl, nrow = 1, ncol = ncol(v), byrow = FALSE, dimnames =list(i))
v <- rbind(v,temp)

} else {
# element is in v but not in A, so add it to A

temp<-matrix(data = repl, nrow = 1, ncol = ncol(A), byrow = FALSE, dimnames =list(i))
A<-rbind(A,temp)
}
}

A<-cbind(A,v[match(rownames(A),rownames(v))])

}

A
}

Note: 09.11.2011: I fixed a bug and added a bit more functionality. You can now tell it, with what you want the missing data to be replaced. Its standard to replace it with NA but you could change it to anything you want.

Code:
 1