Estimating Congressional Results in Different Popular Vote Environments

Published 11/4/2022 | 1,459 Words

Redistricting is the process of redrawing congressional districts after the results of the decennial census are released. This process happens on the state level and has important implications for the balance of power in our republic. Often the redrawing of these districts reshapes the congressional map dramatically as a result of gerrymandering. This gerrymandering can sometimes be a deciding factor when it comes to control of the house.

As such, as the new congressional maps are finalized in the first or second year of each decade, estimating what results can look like based on potential national environments can better inform us as to who is likely to have control of house in congressional sessions to come.

Creating The Map

The first step in order to be able to estimate the house map under different environments is to create a house map that we can shift to different environments. There are many sources of data regarding the new districts, but the best two that I have found are Wikipedia and Dave's Redistricting. The Wikipedia article on the 2022 House of Representatives Elections contains a map of all districts that we can use as a baseline for our map, and Dave's Redistricting contains 2020 presidential results for all of the new districts.

Once the map found on the Wikipedia page is downloaded, we can open it up in our favorite code editor and start removing the svg elements we don't need. All of the elements under the Inserts and Borders group, as well as the style definitions, can be deleted to leave us with the following map:

This map will suit our purposes perfectly. All districts are already labeled with an id tag that corresponds to their district number, which will make the task of formatting our data significantly easier.

Speaking of data, it's time to start compiling all of our district results from Dave's Redistricting. To start this process off, I created a JSON file that will contain all of our data. This JSON file was formatted with an object for each district that contains the 2020 presidential results for the district.

"AL-6": { "demNum": 34.23, "repNum": 64.08},
"AL-7": { "demNum": 65.51, "repNum": 33.42},
"AK-1": { "demNum": 42.77, "repNum": 52.83},
"AZ-1": { "demNum": 50.07, "repNum": 48.65},
"AZ-2": { "demNum": 45.30, "repNum": 53.16},

A portion of the district results data

Once the JSON file is set up, we can start to go map by map in Dave's Redistricting and typing in the district results data that they provide into our JSON file. To get to this data, we must first navigate to a state page, click on their 2022 Congressional Map, and then create an editable copy of the map. This requires an account, but luckily DRA is free and creating an account is 100% worth it. After we have our editable copy of the map, we need to go into the Map Settings and change our primary election dataset from the Composite 2016-2020 dataset to the President 2020 dataset.

After we have changed our dataset, we need to navigate into the Statistics tab and copy the Dem and Rep Partisan Lean numbers into our previously set up JSON data under the demNum and repNum fields respectively. We repeat this process for every state in the nation. For states that only have one congressional district, grabbing state-wide results from another source is also a valid and potentially quicker approach.

Now that we have all of our data compiled into a single file, we need to fill in our map using this data. There are multiple ways to do this, but in my case I created an HTML file with the map SVG embedded in it, then I imported the data by putting it on a web server and loading it through the following JavaScript:

async function requestData(id) {
    let res = await fetch(`https://flizzy.dev/elections/electiondata/${id.substr(0,4)}/${id}.json`);
    return res.json();
}
                
document.addEventListener('DOMContentLoaded', async function() {
    districtResultsOG = await requestData(dataset); //dataset is a string containing whatever we called our json file. In our case it would be something like "2022house"
    districtResults = districtResultsOG; //We manipulate the districtResults variable when we shift our map, we keep districtResultsOG as a copy of the unshifted results.
}, false);

I would not recommend this approach, as it is far more complex than it has to be, but storing the data on a web server at a single point was the best approach for me in order to keep one common set of data wherever I would need it.

To color the districts, we then loop through each object in our JSON we created earlier. We then find the element corresponding to the object key (ex. "AZ-1") and change the element's fill color that based on a blend of colors we find elsewhere (for this exercise, a neutral gray color, red republican color, and blue democratic color). In order to make the map less chaotic, I set an upper bound for when the margin in a district is >15. Once that bound is reached, the district will be filled with the pure democratic or republican color rather than a blend of the neutral color and party color. You could change this 15 value to 100 if you wanted different shades for every margin.

async function colorDistricts() {
    for(district in districtResults.districts) { //district == object key
        let mapSvg = document.getElementById("mapsvg");
        let districtPath = mapSvg.getElementById(district);
        let color = "#999999"; //Initialize a color just in case repNum == demNum
        let demNum = Number(districtResults.districts[district].demNum);
        let repNum = Number(districtResults.districts[district].repNum);
        if (demNum > repNum) {
            let diff = demNum - repNum
            diff = diff / 15 //For the blend function, a diff of 1 means 100% of the second color is used. We divide by 15 here to get a decimal number so that 5% margin will blend 0.33 or 33% between both colors.
            if (diff > 1) {
                color = demColor;
            } else {
                diff = 1 - diff; //Subtract diff from 1 to get what we need to blend each color by. We do this because 1 means all second color.
                color = blendColors(demColor, demMidColor, diff)
            }
        } else if (repNum > demNum) {
            let diff = repNum - demNum
            diff = diff / 15
            if (diff > 1) {
                color = repColor;
            } else {
                diff = 1 - diff;
                color = blendColors(repColor, repMidColor, diff)
            }
        }
        districtPath.style.fill = color; //Set the fill color of the district.
    }
}

function blendColors(colorA, colorB, amount) { //Amount is anywhere from 0 to 1, 1 meaning all second color, 0 being all first color.
    const [rA, gA, bA] = colorA.match(/\w\w/g).map((c) => parseInt(c, 16));
    const [rB, gB, bB] = colorB.match(/\w\w/g).map((c) => parseInt(c, 16));
    const r = Math.round(rA + (rB - rA) * amount).toString(16).padStart(2, '0');
    const g = Math.round(gA + (gB - gA) * amount).toString(16).padStart(2, '0');
    const b = Math.round(bA + (bB - bA) * amount).toString(16).padStart(2, '0');
    return '#' + r + g + b; //Return in proper format for a hex color.
}

The function used to color districts

Once we load in our data and run the color function, we get the following map:

Voila! Now we have a map of the 2022 congressional districts shaded by how they voted in the 2020 presidential election. Unfortunately though, we can't just stop here. To create a map that represents the outcome of a neutral year, where neither party has an advantage in the house popular vote, we must shift this map 4.4 points to the right, as that is how much Biden won the popular vote by in 2020.

Shifting The Map

Once we have a map of performance in 2020, we have a good baseline for how these districts would vote in a year like 2020. This method of partisan lean does have multiple drawbacks, notably it does not account for the effects of downballot lag or candidate quality. The Cook PVI does a good job in accounting for some of these factors, and if we wanted an even more accurate map, using a metric like it would serve us well.

To get from this environment where Democrats won the popular vote by 4.4 points to an environment where neither party has an advantage, we need to shift each district right by 4.4 points. We can do this pretty simply by increasing the Republican vote share in each district by 2.2 points and reducing the Democratic vote share by the same 2.2 points. We do this by looping through the districtResults object that we defined before and dong that exact math. At the end, when we have a fully looped through object, we call the colorDistricts function.

async function changeResults(valStr) { //valStr is a string that contains a number within it, so "0" or "-2". 
    //Positive numbers are Democratic margin, so for D+5 you would enter 0.6 (4.4+0.6). Republicans are negative. 
    let val = Number(valStr); //Convert valStr to a number we can work with
    districtResults = JSON.parse(JSON.stringify(districtResultsOG)); //Copy in original district results
    let demSeats = 0;
    let repSeats = 0;
    for(district in districtResults.districts) { //Loop through district object, district == object key
        let demNum = Number(districtResults.districts[district].demNum)
        let repNum = Number(districtResults.districts[district].repNum)
        demNum += (val/2); //Add half of valStr to dem, so 0.6 entered, add 0.3
        repNum -= (val/2); //Remove the other half of the margin shift from reps.
        districtResults.districts[district].demNum = demNum.toFixed(3);
        districtResults.districts[district].repNum = repNum.toFixed(3);
        if (demNum > repNum) { //This function also keeps track of the total amount of districts each party wins
            demSeats += 1;
        } else if (demNum < repNum) {
            repSeats += 1;
        } else { //If a seat ties it will throw it out
            console.log(`tie in ${district}`)
        }
    }
    districtResults.demSeats = demSeats.toFixed(0); //Thinking there might still be round off error
    districtResults.repSeats = repSeats.toFixed(0);
    colorDistricts(); //Color our districts using new margins
}

The function used to shift results

Based on how the argument for changeResults works, in order to get a neutral year, we must enter "-4.4" at the valStr argument. This negative tells us in which direction to shift (negative Rs, positive Ds) and the number tells us how many points to shift. When we enter "-4.4" into our function, we get the following map:

Estimated house map in a neutral year

This is our neutral map! This map also happens to be how left or right of the nation a district is. Using the changeResults function with different arguments, we can see how the house results might look in different environments, for example, if we use a "-9.4" value for valStr, we can estimate the house outcome if Republicans win the house popular vote by 5 percentage points:

Estimated house map in an R+5 year

Why This?

Using this technique, we can estimate the house outcome based on either our predictions or more concrete values like the Real Clear Politics or FiveThirtyEight generic congressional ballot averages, which are based on polling conducted nationwide. This can help us get a better grasp of the state of the race for the house and which party is likely to win. This can be especially important in a year like 2022 where there is very little polling in house races and most of the polling that we do have is conducted by partisan pollsters working for invested parties.

That being said, as acknowledged above, this method is not close to perfect. If it was, you wouldn't see election race ratings from outlets from the Cook Political Report, Split Ticket or FiveThirtyEight. Partisanship these days dictates a lot in politics, but we have not yet, and probably will never, reach a point where other factors don't exist. And frankly, if we do ever reach that point, it would be pretty boring.

Uncertainty is simultaneously terrifying and exciting. It gives us an incentive to innovate and remove just some of that terrifying uncertainty from our lives. With that understanding, when faced with uncertainty, there is no need to give up. Instead of constantly living in the fear of the unknown, we should use it as inspiration. We should ask "why?" instead of throwing in the towel. If we do, we can try, try, and try again until we remove all of the uncertainty that we can and expand our understanding of the world around us.

Blog Home | Site Home