What is the House Freedom Caucus?
I read in the venerable Sacramento Bee (via AP) that impeachment articles have been brought up against Deputy Attorney General Rod Rosenstein. Completely ignoring the merits of the effort, the first thing I noticed were the leaders. The articles were introduced by Mark Meadows and Jim Jordan. Both are Republicans and leaders of the "Freedom Caucus". And both seem to get a lot of press coverage. And just today I saw that Jordan is vying to replace Paul Ryan as Speaker of the House.
{ function calculateAge(birthday) { // birthday is a date var ageDifMs = Date.now() - birthday.getTime(); var ageDate = new Date(ageDifMs); // miliseconds from epoch return Math.abs(ageDate.getUTCFullYear() - 1970); } const ageMedian = d3.median(freedomMembers.map(m => { const bday = new Date(m.date_of_birth) return calculateAge(bday) })) const dudeCount = freedomMembers.filter(m => m.gender === 'M').length return md`The Freedom Caucus is a powerful bunch of Republican House members. They are mostly old (median age is ${ageMedian} years old) dudes (${dudeCount} out of ${freedomMembers.length} are men). They are powerful because when 80% of the caucus votes for a demand, the entire caucus votes as a bloc.` }
{ return md`Their influence comes from math and that solidarity. The Republicans have ${republicans.length} seats, which is ${republicans.length - 217} seats more than they need for a majority. But the ${freedomMembers.length} Freedom Caucus members can dash that majority by voting together.` }
{ const median = d3.median(freedomMembers.map(d => 2018 - d.elected_in)) return md`Unlike the Tea Party (which is neither an official caucus nor an official party), the members of the Freedom Caucus were not all elected in one wave election. Many were elected recently as evidenced by the median tenure being ${median} years.` }
{ const padding = 50 const width = 700 const barsWidth = width * .81 const rectHeight = 10 const rectPadding = 2 const height = 450 const svg = d3.select(DOM.svg(width, height)) const sorted = freedomMembers.sort((a, b) => { return b.elected_in - a.elected_in }) const electedIns = freedomMembers.map(d => +d.elected_in) const x = d3.scaleLinear().domain([d3.min(electedIns), 2018]).range([0, barsWidth]) const xAxis = d3.axisBottom(x).tickFormat(d3.format('2')) const termLinesG = svg.append('g') d3.range((2018 - d3.min(electedIns)) / 2).map(term => { const termYear = d3.min(electedIns) + (term * 2) const termLineWidth = 2 const termX = x(termYear) termLinesG.append('line') .attr('x1', termX) .attr('x2', termX) .attr('y1', '0') .attr('y2', height - padding - rectPadding) .attr('stroke', gray) }) const bars = svg.append('g') .selectAll('g') .data(sorted) .enter() .append('g') .attr('class', 'bar') .style('border', '1px solid black') bars.append('rect') .attr('y', (d, i) => i * (rectHeight + rectPadding)) .attr('x', d => x(+d.elected_in)) .attr('width', d => ( x(2018) - x(+d.elected_in) )) .attr('height', 10) .attr('fill', 'rgb(162, 92, 92)') const fontSize = 9 bars.append('text') .attr('x', barsWidth + rectHeight) .attr('y', (d, i) => i * (rectHeight + rectPadding) + fontSize) .attr('font-size', fontSize) .attr('font-family', 'sans-serif') .text(d => `${d.short_title} ${d.first_name} ${d.last_name} (${d.state})`) svg.append('g') .attr('transform', `translate(0, ${height - padding})`) .call(xAxis) .append('text') .attr('fill', 'black') .attr('font-weight', '800') .attr('x', barsWidth / 2) .attr('y', padding / 2 + rectPadding) .text('Years in Congress') const median = d3.median(freedomMembers.map(d => 2018 - d.elected_in)) const medianG = svg.append('g') medianG.append('rect') .attr('x', x(2018 - median)) .attr('y', 0) .attr('height', (sorted.length) * (rectHeight + rectPadding)) .attr('width', '2') .attr('fill', gray) medianG.append('text') .attr('font-family', 'sans-serif') .attr('font-size', 13) .attr('fill', 'black') .attr('x', x(2018 - median * 2.2)) .attr('y', (height / 2) - padding) .text(`Median: ${median} years`) return svg.node() }
{ const format = d3.format('.2') const freedomIds = freedomMembers.map(d => d.id) const sortPartyPct = (a, b) => a.votes_with_party_pct - b.votes_with_party_pct const lowestFreedom = freedomMembers.sort(sortPartyPct)[0] const meanFreedom = d3.mean(freedomMembers.map(d => d.votes_with_party_pct)) const stdFreedom = d3.deviation(freedomMembers, d => d.votes_with_party_pct) const stdLowestFreedom = (meanFreedom - lowestFreedom.votes_with_party_pct) / stdFreedom const other = republicans.filter(d => !freedomIds.includes(d.id)) const lowestOther = other.sort(sortPartyPct)[0] const medianOther = d3.median(other.map(d => d.votes_with_party_pct)) return md`According to data from ProPublica, it doesn't seem like there's an obvious trend to the party loyalty of Freedom Caucus members. Despite their reputation as rennegages, on average the Freedom Caucus members vote with the party ${format(meanFreedom)}% of the time. Across the party, an average Republican votes with their party ${format(medianOther)}% of the time. Individually, ${lowestFreedom.first_name} ${lowestFreedom.last_name} (${lowestFreedom.state}) votes with the Republican party the least. ${lowestFreedom.gender === 'F' ? 'She' : 'He'} votes with the party ${format(lowestFreedom.votes_with_party_pct)}% of the time. But that's pretty crazy, even for the Freedom Caucus, and is ${format(stdLowestFreedom)} standard deviations away from the caucus' mean. And just bucking the establishment doesn't mean somebody is in the Freedom Caucus. ${lowestOther.first_name} ${lowestOther.last_name} (${lowestOther.state}) is the Republican who votes the least with ${lowestFreedom.gender === 'F' ? 'her' : 'his'} party and is **not** a member. ${lowestFreedom.gender === 'F' ? 'She' : 'He'} only votes with the leadership ${format(lowestOther.votes_with_party_pct)}% of the time. ` }
Unsuprisingly, Trump did well in districts represented by the Freedom Caucus. Using data from Daily Kos we can see that Freedom Caucus districts voted for Trump in 2016 and Romney in 2012 by a healthy margin. This makes sense since the Freedom Caucus demands are among the most conservative policy proposals.
vegalite({ data: { values: data }, layer: [ { mark: 'point', encoding: { x: { field: 'Trump2016Margin', type: 'quantitative', axis: { title: 'Trump\'s margin of victory in 2016 (percent)' } }, y: { field: 'Obama2012Margin', type: 'quantitative', axis: { title: 'Obama\'s margin of victory in 2012 (percent)' } }, fill: { field: 'FreedomMember', type: 'nominal' }, tooltip: [ { field: 'CD', type: 'nominal' }, { field: 'Incumbent', type: 'nominal' }, { field: 'Trump2016Margin', type: 'quantitative' }, { field: 'FreedomMember', type: 'nominal' } ] } }, { mark: 'rule', encoding: { x: { value: 0 }, color: { value: 'red' }, } } ], config: { "view": { "width": "700", "height": "400" } } })
{ const noFreedom = byState.filter(s => s.value.percent === 0) const max = d3.max(byState, s => s.value.percent) const state = byState.find(s => s.value.percent === max) const stateName = d3.entries(us).find(d => d.key === state.key).value return md`Most states have no representation in the Freedom Caucus (${noFreedom.length} states), though ${stateName} is most represented as ${max}% of its congressional seats are held by Freedom Caucus members. This analysis favors classifying small states but, _honestly_, what in American politics doesn't? This cartogram shows the districts across the country that are represented by Freedom Caucus members. From this perspective, their power is very clear. With just a smattering of districts (and the power that comes by being affiliated with a Congressional majority) the Freedom Caucus is able to throw its weight around like [when it removed John Boehner as Speaker of the House](https://fivethirtyeight.com/features/the-hard-line-republicans-who-pushed-john-boehner-out/).` }
{ const width = 700 const height = 450 const svg = d3.select(DOM.svg(width, height)) const transform = d3.geoTransform({ point: function(x, y) { this.stream.point(x, -y) } }) const path = d3.geoPath().projection(transform) const g = svg.append('g').attr('transform', `translate(0,${height / 1.3 }) scale(.3)`) const usAbbrs = d3.entries(us) const hexFeatures = topojson.feature(usHexTiles, usHexTiles.objects.tiles).features hexFeatures.forEach(feature => { const f = g.append('g').attr('id', feature.properties.name) console.log({ coords: feature.geometry.coordinates }) f.selectAll('path') .data(feature.geometry.coordinates) .enter() .append('path') .attr('d', d => path({ type: 'Polygon', coordinates: d.length > 1 ? [d] : d })) .attr('data-d', d => d.length) .attr('class', 'hex-path') .attr('fill', (d, i) => { const state = feature.properties.name const abbr = usAbbrs.find(d => d.value.toUpperCase() === state).key const isFreedom = freedomMembers.find(d => d.state === abbr && +d.district === i + 1) if (isFreedom) return 'rgb(238, 173, 117)' return 'rgb(137, 161, 191)' }) .attr('stroke', 'white') .attr('data-district', (d, i) => i + 1) }) const stateBorders = usAbbrs.map(d => { return topojson.merge(usHexTiles, usHexTiles.objects.tiles.geometries.filter(geometry => ( geometry.properties.name === d.value.toUpperCase() ))) }) g.append('g') .selectAll('path') .data(stateBorders) .enter() .append('path') .attr('d', path) .attr('stroke', 'black') .attr('stroke-width', '3') .attr('fill', 'none') return svg.node() }
Each hexagon represents a congressional district. The orangey ones are held by Freedom Caucus members while the blue hexagons are not.
In a way, the way the Freedom Caucus operates is very much like that of a coalition party in a parliamentry system. They generally vote with the leaders (which gives both them and the leadership power) but can be a very sharp thorn in the side of the governing. The Freedom Caucus has power not because they have a huge membership but because of how cohesive the group is. That makes me wonder what sort of conversations were had when the Caucus was recruiting members and what convinced them to bind themselves first and foremost to decisions made by 80% of the Caucus.
gray = 'rgb(221, 221, 221)'
byState = { const freedomIds = freedomMembers.map(m => m.id) const inOffice = congress.results[0].members.filter(m => m.in_office) const nest = d3.nest() .key(d => d.state) .rollup(l => { const freedomCount = l.filter(d => freedomIds.includes(d.id)).length return { freedomCount, count: l.length, percent: (freedomCount / l.length) * 100 } }) .entries(inOffice) return nest }
data = resultData.map(d => { const isFreedomMember = freedomMembers.find(m => { const cd = +m.district < 10 ? `0${m.district}` : m.district return d.CD === `${m.state}-${cd}` }) return { ...d, FreedomMember: !!isFreedomMember } })
freedomMembers = d3.csv('https://gist.githubusercontent.com/jeremiak/1c986c7b533dd1dc271e48e10c37adf5/raw/ccc7655557d378fe7c620ca893da5373ce9a64fc/freedom-caucus-members.csv').then(d => { return d.map(row => { const match = congress.results[0].members.find(m => ( m.state === row.State && m.district === row.CD && m.in_office )) return { ...match, elected_in: +row['Elected in'] } }) })
resultData = d3.json('https://gist.githubusercontent.com/jeremiak/65a2895f59526ec1e6baadd67f5b48a4/raw/880c6bbfd36fc7865b2331f0ad724b983363d145/daily-kos-presidential-election-results-by-cd.json').then(data => { return data.map(d => ({ ...d, State: d.CD.split('-')[0], Clinton2016: +d.Clinton2016, Trump2016: +d.Trump2016, Trump2016Margin: +d.Trump2016 - +d.Clinton2016, Obama2012: +d.Obama2012, Romney2012: +d.Romney2012, Obama2012Margin: +d.Obama2012 - +d.Romney2012, Obama2008: +d.Obama2008, McCain2008: +d.McCain2008, Obama2008Margin: +d.Obama2008 - +d.McCain2008, })) })
republicans = congress.results[0].members.filter(m => m.party === 'R' && m.in_office)
congress = d3.json('https://gist.githubusercontent.com/jeremiak/1c986c7b533dd1dc271e48e10c37adf5/raw/87c8473a3a209902996601d2333daa0e85c5234c/congress-115-members-propublica.json')
usHexTiles = d3.json('https://gist.githubusercontent.com/jeremiak/1c986c7b533dd1dc271e48e10c37adf5/raw/58a3dfce70ef6c32ba2b941f35471c5b0da0dfbf/us-hex-tiles.topo.json')
vegalite = require('@observablehq/vega-lite@0.1')
us = { return { AL: 'Alabama', AK: 'Alaska', AS: 'American Samoa', AZ: 'Arizona', AR: 'Arkansas', CA: 'California', CO: 'Colorado', CT: 'Connecticut', DE: 'Delaware', DC: 'District Of Columbia', FM: 'Federated States Of Micronesia', FL: 'Florida', GA: 'Georgia', GU: 'Guam', HI: 'Hawaii', ID: 'Idaho', IL: 'Illinois', IN: 'Indiana', IA: 'Iowa', KS: 'Kansas', KY: 'Kentucky', LA: 'Louisiana', ME: 'Maine', MH: 'Marshall Islands', MD: 'Maryland', MA: 'Massachusetts', MI: 'Michigan', MN: 'Minnesota', MS: 'Mississippi', MO: 'Missouri', MT: 'Montana', NE: 'Nebraska', NV: 'Nevada', NH: 'New Hampshire', NJ: 'New Jersey', NM: 'New Mexico', NY: 'New York', NC: 'North Carolina', ND: 'North Dakota', MP: 'Northern Mariana Islands', OH: 'Ohio', OK: 'Oklahoma', OR: 'Oregon', PW: 'Palau', PA: 'Pennsylvania', PR: 'Puerto Rico', RI: 'Rhode Island', SC: 'South Carolina', SD: 'South Dakota', TN: 'Tennessee', TX: 'Texas', UT: 'Utah', VT: 'Vermont', VI: 'Virgin Islands', VA: 'Virginia', WA: 'Washington', WV: 'West Virginia', WI: 'Wisconsin', WY: 'Wyoming', } }
topojson = require('topojson-client')
d3 = require('d3')