Mapping Congressional Districts in R with the ggplot2 package

114th House Election ResultsI’m often asked how I’m able to produce maps of Congressional districts in R.

R, for the blissfully uninitiated, is a statistical programming environment that’s about as fun to use as undergoing a colonoscopy. Its bizarre syntax, unhelpful errors and warnings, unpredictable behavior, terrible regex support, slowness (perhaps because it was never intended as a fully-featured development tool), and polluted namespace are bad enough. Even worse is that the support community sometimes has the social graces of Unix-bearded cheese graters (imagine the world’s crankiest professors), as this example illustrates. Python takes days to comfortably grasp; R can take years.

Nonetheless, for flexibility and cost (you get what you pay for: it’s free), R surpasses Stata (easier to learn, but single-threaded and limited) and other closed-source packages for econometric statistical programming.

The best illustrations of R’s power are the beautiful and infinitely customizable ways you can present data and models with third-party libraries. ggplot2 is one of the most comprehensive of these, as well as one of the easiest to use (a low bar, granted). Here, I’ll quickly show you how to map discrete values (in this case, election results) on extant (114th) Congressional districts. The polygons for Alaska and Hawaii are moved into the viewing range of the map.

Steps

1. Use the install.packages command to install the following: maptools, ggplot2, rgdal, rgeos, mapproj

2. Create a directory on your hard-drive (e.g. “map”) to store the R script. Included in the directory should be the following subdirectories: bin, maps, plots, and shp. “bin” will store binaries of the map dataframe for later use, just in case you want to make changes to the output but don’t want to tortuously wait for R to import and convert the shapefile. “maps” will store the output. “plots” will store the values with which we wish to fill. “shp” will store the GIS files.

3. Save the plot data (the 2014 House election returns) into the “plots” folder.

5. Download the 114th Congressional district shapefile from Lewis et al. at http://cdmaps.polisci.ucla.edu/. Extract to the folder “shp/114.”

5. Save the following code into the main directory as “map.R.”

###############################################
#                                             #
# Map Congressional district election results #
#                                             #
# By Clifford Vickrey                         #
# Ph.D., Phil Collins Studies                 #
#                                             #
###############################################

# NOTE: tested in R 3.0.1 and later

############
# SETTINGS #
############

# stack tracing: on
options(error=traceback)

# Congress number
cong<-114

# move non-contiguous states into viewport?
move.non.contiguous<-TRUE

# load binary map datafame if it exists?
load.bin<-TRUE

# get working directory
# this appears needlessly complicated, but is necessary if you want to run the
# script from either R's IDE *or* from the command line using RScript
wd<-dirname(
    tryCatch(
        normalizePath(parent.frame(2)$ofile),
        error=function(e)
            normalizePath(
                unlist(
                    strsplit(
                        commandArgs()[
                            grep('^--file=',commandArgs())
                        ],
                        '='
                    )
                )[2]
            )
    )
)

# paths for the files
shp.path<-paste(wd,'shp',cong,sep='/')
bin.path<-paste(wd,'bin',paste(cong,'house','RData',sep='.'),sep='/')
dat.path<-paste(wd,'plots',paste(cong,'house_','csv',sep='.'),sep='/')
map.path<-paste(wd,'maps',paste(cong,'house','png',sep='.'),sep='/')

############
# INCLUDES #
############

suppressWarnings(suppressPackageStartupMessages(require(maptools)))
suppressWarnings(suppressPackageStartupMessages(require(ggplot2)))
suppressWarnings(suppressPackageStartupMessages(require(rgdal)))
suppressWarnings(suppressPackageStartupMessages(require(rgeos)))
suppressWarnings(suppressPackageStartupMessages(require(mapproj)))

#############
# FUNCTIONS #
#############

# elide a state polygon
# parameters:
# 1 = rotation
# 2 = resize
# 3 = shift coordinates to (lat, long)
elide.state.polygon<-function(object,params){
    r=params[1]
    e.scale=params[2]
    shift=params[3:4]
    object=elide(object,rotate=r)
    size=max(apply(bbox(object),1,diff))/e.scale
    object=elide(object,scale=size)
    object=elide(object,shift=shift)
    object
}

###################
# PARSE SHAPEFILE #
###################

cat('\nMaking maps for Congress #',cong,' ...\n\n',sep='')

{
    # first, see if the map data frame has already been saved as a binary
    if(load.bin&file.exists(bin.path)){
        load(bin.path,.GlobalEnv)
    }
    # otherwise load the shapefile; convert it into a data frame
    else{
        # hide annoying warnings about NULL geographies
        usa<-suppressWarnings(
            readOGR(
                dsn=shp.path,
                layer=paste0('districts',sprintf('%03.0f',cong)),
                verbose=FALSE
            )
        )
        
        # move non-contiguous states, or otherwise delete them
        if(move.non.contiguous){
            # move Alaska
            alaska=elide.state.polygon(
                usa[usa$STATENAME=='Alaska',],
                c(0,2.5,-130,22)
            )
            proj4string(alaska)<-proj4string(usa)
            
            # move Hawaii
            hawaii=elide.state.polygon(
                usa[usa$STATENAME=='Hawaii',],
                c(0,1,53,5)
            )
            proj4string(hawaii)<-proj4string(usa)
            
            # rebuild United States
            usa=rbind(
                usa[!usa$STATENAME %in% c('Alaska','Hawaii'),],
                alaska,hawaii
            )
        }
        else{
            usa<-usa[!usa$STATENAME %in% c('Alaska','Hawaii'),]
        }
        
        # convert map object to dataframe, merge in district identifiers
        # drop all other jurisdictional data because R is a RAM hog
        the.map<-fortify(usa)
        the.map<-data.frame(the.map,usa@data[the.map$id,c('ID')])
        names(the.map)[length(names(the.map))]<-'district.id'
        rm(usa)
        
        # make map geo data consistent with plot
        # area.id = alphabetical list of state/CD combos (Alabama 01, Alabama 02,
        # etc.), excluding Washington, D.C. (1:435)
        the.map$area.id<-match(
            as.numeric(the.map$district.id),
            sort(unique(as.numeric(the.map$district.id)))
        )
        the.map<-subset(the.map,select=-district.id)
        
        # save the map!
        save(the.map,file=bin.path)
    }
}

# merge in the plot data
the.plot<-read.csv(dat.path,stringsAsFactors=FALSE)
names(the.plot)=gsub('_','.',names(the.plot))
the.map$result<-the.plot$result[the.map$area.id]
the.map<-subset(the.map,select=-area.id)

# edit viewport
the.map<-the.map[the.map$long<0,]
the.map<-the.map[the.map$long>-135,]
the.map<-the.map[the.map$lat<50,]
the.map<-the.map[the.map$lat>22,]

####################
# MAP THEME/COLORS #
####################

# plot theme object
map.theme<-theme_bw()
map.theme$line<-element_blank()
map.theme$rect<-element_blank()
map.theme$strip.text<-element_blank()
map.theme$axis.text<-element_blank()
map.theme$axis.title<-element_blank()
map.theme$legend.position='none'

# fill palette
# 1 = Democratic hold (pale blue)
# 2 = Democratic gain (blue)
# 3 = Republican hold (pink)
# 4 = Republican gain (red)
# 5 = Independent hold (light green)
# 6 = Independent gain (green)
fill.palette<-
    c('#CCCCFF','#9999FF','#FFCCCC','#FF9999','#CCFFCC','#99FF99')[
        sort(unique(the.plot$result))
    ]

################
# PLOT THE MAP #
################

# NOTE: here, I fill with factors of the variable; otherwise,
# scale_fill_manual won't work
map.plot<-
    ggplot(the.map)+
    geom_polygon(
        aes(
            x=long,
            y=lat,
            group=group,
            fill=factor(the.map$result)
        ),
        colour='black',lwd=1/9
    )+
    coord_map(project='conic',lat0=30)+
    map.theme+
    scale_fill_manual(
        values=fill.palette
    )

# save!
ggsave(map.path,map.plot,h=9,w=16,type='cairo-png')
cat('\nI ate the bones!\n')

6. Execute the script in R with the command

source('/path/to/the/map/map.R')

where /path/to/etc. is the location of the script on your hard drive.

7. The basic map should appear as maps/114.house.png. For customization options, a good primer is available at http://zevross.com/blog/2014/07/16/mapping-in-r-using-the-ggplot2-package/

About cvickrey

Clifford Vickrey spends his days confounding the wise.
This entry was posted in political science. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *