There’s an old joke about how algebra was created when the Devil suggested putting the alphabet into math. R-based text analysis involves doing something like the opposite: Putting math into the alphabet.
The general idea is that the more often a word appears in the text of a document, the likelier it is that the document is about whatever idea that word represents. So, while reading a document is usually the best way to understand its meaning, looking at its word counts can offer a pretty good shortcut.
Consider Lincoln’s 271-word Gettysburg Address. The most common non-trivial word in the address is “here,” which occurs eight times. Next is “nation” (five times), followed by “dedicated” (four times). Knowing these words and their frequency counts certainly cannot get the full meaning and nuance of the speech. But it can help you get the idea that Lincoln was saying something about “dedication” and a “here” that was important to the “nation.”
R can quickly produce such word counts, even for documents many times the length of Lincoln’s famous address.
As an introduction to what’s possible, let’s look at word frequency counts for everything members of the U.S. Congress posted in X, formerly called Twitter, between Feb. 28, 2026, the day the U.S. and Israel launched attacks on Iran, and April 15, 2026, a few days into a ceasefire.
Below are the top 20 words used by Republicans during the period, followed by the top 20 words used by Democrats and independents during the period.
What do the two graphs suggest to you about what members of each party were posting about as the hostilities and ceasefire were unfolding?
I downloaded the posts using Brandwatch, a proprietary - and, I’m afraid, very expensive - social media content monitoring platform available to students and faculty in Middle Tennessee State University’s School of Journalism and Strategic Media.
Using Brandwatch is beyond the scope of this course. But I have extracted the posts described above, added some information about each post’s source, and put the data on my GitHub page. Let’s get it and have a look how you can use it to make the graphics shown.
This code will install, if needed, and load some required
packages. Then, it will download the posts
from a file in my GitHub space and bring it into R as a data frame
called XData.
One new package here is tidytext, the
go-to package for basic text mining in R. Another is
kableExtra, a lightweight package for
making nice-looking tables. By now, you should
recognize the tidyverse code enhancement package and the
plotly interactive graphics package.
# ----------------------------------------------------------
# Step 1: Install required packages (if missing)
# ----------------------------------------------------------
if (!require("tidyverse")) install.packages("tidyverse")
if (!require("plotly")) install.packages("plotly")
if (!require("tidytext")) install.packages("tidytext")
if (!require("kableExtra")) install.packages("kableExtra")
library(tidyverse)
library(plotly)
library(tidytext)
library(kableExtra)
# ----------------------------------------------------------
# Step 2: Load data
# ----------------------------------------------------------
XData <- read_csv(
"https://github.com/drkblake/Data/raw/refs/heads/main/IranWarCongressX.csv",
col_types = cols(
Date = col_date()
)
)
I get an XData data frame with 56,718
rows, with eight columns showing:
The Date of each post.
The Full.Name of the post’s author
An Author column containing the author’s X.com handle
The Url for the post. If pasted into a browser, this URL will show the post, assuming the post is still available on X.com.
The Full.Text of each post.
The party of the post’s author, either “Rep” for Republican or “Dem/Ind” for Democratic or independent. Brandwatch didn’t provide this info. I added it.
The chamber in which the author serves, either the “House” or the “Senate.” I added this information, too.
The state the author represents in Congress. Again, I added this information to the Brandwatch data.
What happens next is something you should be glad your computer will do for you so that you don’t have to do it yourself.
When you run the code below, your computer will:
Create a data frame called X_words
that lists every word in each of those
56,718 posts, along with (for each word) the
Date, Full.Name, Author,
Url, party, chamber, and
state data for the post in which the word appeared. That’s
nearly 2.5 million words. That’s what’s happening on
Step 3 of the code. At the heart of the operation is
the unnest_tokens() function from the
tidytext package. In text mining lingo,
tokens are whatever parts you break a body of
text into - in this case, individual words. As you will see
later, it’s also possible to break the text into other tokens, like
multi-word phrases. The “nest” part of the function reflects a broader
data science concept of taking apart nested data structures and
flattening the data into simple data frame rows.
Delete all stop words, like “the,” “an,” “but,” and so on that get used frequently but don’t hold any particular meaning by themselves. That’s Step 4 in the code.
Also delete other words and character strings like “twitter,” “t.co”, and “www” that, like stop words, show up frequently in X.com content but aren’t useful for inferring meaning. It also removes some frequent, but individually unhelpful, words, like “I’m” and “it’s.” You could specify other words to omit if you wanted to. Step 5 in the code handles these tasks.
Use what’s left to make a data frame called
party_word_counts that will show you a
count of how many times each word appeared in posts by
Republicans and in posts by Democrats or
independents. In the code, Step 6 produces the
data frame.
Here’s the code:
# ----------------------------------------------------------
# Step 3: Tokenize Full.Text into single words
# ----------------------------------------------------------
XData <- XData %>%
mutate(
Full.Text = str_replace_all(Full.Text, "[’‘`]", "")
)
X_words <- XData %>%
unnest_tokens(word, Full.Text)
# ----------------------------------------------------------
# Step 4: Remove stop words
# ----------------------------------------------------------
data("stop_words")
X_words_clean <- X_words %>%
anti_join(stop_words, by = "word")
# ----------------------------------------------------------
# Step 5: Remove X.com–specific noise and contractions
# ----------------------------------------------------------
x_noise <- c(
"https", "tco", "t.co", "rt", "amp",
"co", "com", "www",
"twitter", "x",
"im", "its"
)
X_words_clean <- X_words_clean %>%
filter(
!str_detect(word, "^@"),
!str_detect(word, "^#"),
!word %in% x_noise,
str_detect(word, "[a-z]"),
nchar(word) > 2
)
# ----------------------------------------------------------
# Step 6: Count word usage by party
# ----------------------------------------------------------
party_word_counts <- X_words_clean %>%
count(party, word, sort = TRUE)
If you open the party_word_counts data frame in RStudio,
you’ll see a row for each word mentioned in
each party’s posts and a count of how
many times a party’s posts mentioned the word.
The first row, for example, tells you that the word “war” appeared 6,590 times in posts by Democrats and independents - more times than any other word they used.
Among Republicans, the most-used single word was “American,” with 4,801 mentions, followed closely by the nearly identical “America,” with 3,986 mentions.
Remember: That 6,590 number counts the number of times Democrats and independents used the word “war.” It does not, however, count the number of posts by Democrats and independents that used the word “war.”
It is certainly possible to get that count, and the two counts are probably similar. But they are not interchangeable, and reporting your results accurately requires keeping the difference between them in mind.
A simple example might help. Imagine two posts. One reads, “I support the war.” The other reads, “All they want is war, war, war.” That would be two posts mentioning “war,” but four mentions of “war.”
If you use the sorting capability in RStudio’s viewer to sort the data frame by the word column, then scroll (way) down to where the rows for the word “war” appear, you can see that while Democrats and independents used the word 6,590 times, Republicans used it 928 times:
Is the difference between the counts trivial? Maybe. But news outlets including The New York Times, CNN, CBS News, and The Hill reported that Republicans in Congress had political and legal reasons for avoiding using the word “war” during the opening weeks of the conflict.
One way to share at least some of your results with an audience would be to show them a table of the top 20 words used by each party. Steps 7 and 8 in the code will make and display such a table:
# ----------------------------------------------------------
# Step 7: Identify TOP 10 words for each party
# ----------------------------------------------------------
top_words_by_party <- party_word_counts %>%
group_by(party) %>%
slice_max(n, n = 20) %>%
ungroup()
# ----------------------------------------------------------
# Step 8: Display table
# ----------------------------------------------------------
WordTable <- top_words_by_party %>%
arrange(party, desc(n)) %>%
kable(
col.names = c("Party", "Word", "Mentions"),
caption = "Most Common Words in Congressional X Posts by Party (Top 10)"
) %>%
kable_styling(full_width = FALSE)
WordTable
| Party | Word | Mentions |
|---|---|---|
| Dem/Ind | war | 6590 |
| Dem/Ind | trump | 5987 |
| Dem/Ind | people | 3440 |
| Dem/Ind | american | 3432 |
| Dem/Ind | iran | 3417 |
| Dem/Ind | americans | 3066 |
| Dem/Ind | families | 2761 |
| Dem/Ind | congress | 2634 |
| Dem/Ind | republicans | 2597 |
| Dem/Ind | president | 2495 |
| Dem/Ind | care | 2365 |
| Dem/Ind | day | 2312 |
| Dem/Ind | ice | 2172 |
| Dem/Ind | trumps | 2033 |
| Dem/Ind | health | 2004 |
| Dem/Ind | community | 1964 |
| Dem/Ind | act | 1910 |
| Dem/Ind | time | 1758 |
| Dem/Ind | bill | 1744 |
| Dem/Ind | costs | 1618 |
| Rep | american | 4801 |
| Rep | america | 3986 |
| Rep | act | 3859 |
| Rep | americans | 3836 |
| Rep | democrats | 3736 |
| Rep | tax | 3622 |
| Rep | families | 3389 |
| Rep | security | 2953 |
| Rep | people | 2913 |
| Rep | president | 2855 |
| Rep | u.s | 2606 |
| Rep | day | 2515 |
| Rep | time | 2383 |
| Rep | dhs | 2213 |
| Rep | support | 2152 |
| Rep | country | 2114 |
| Rep | trump | 2069 |
| Rep | iran | 2043 |
| Rep | senate | 1967 |
| Rep | bill | 1906 |
The table is informative, perhaps, but kind of boring-looking. Presenting the same results using horizontal bar charts - one for Republicans, and a second for Democrats and independents - would look a little snazzier. Step 9 produces and shows the charts.
It is possible to arrange the charts side-by-side, in a single graphic. But the bar labels end up small, and the whole thing looks kind of crowded. I think separate charts work better.
# ----------------------------------------------------------
# Step 9: Plotly visualization
# ----------------------------------------------------------
rep_data <- top_words_by_party %>%
filter(party == "Rep") %>%
arrange(desc(n))
dem_data <- top_words_by_party %>%
filter(party == "Dem/Ind") %>%
arrange(desc(n))
rep_plot <- plot_ly(
data = rep_data,
x = ~n,
y = ~word,
type = "bar",
orientation = "h",
marker = list(color = "#c0392b")
) %>%
plotly::layout(
title = list(text = "Top Words used by Republicans", font = list(size = 18)),
xaxis = list(
title = "",
tickfont = list(size = 14)
),
yaxis = list(
title = "",
tickfont = list(size = 14),
categoryorder = "array",
categoryarray = rev(rep_data$word)
),
margin = list(l = 140)
)
rep_plot
dem_plot <- plot_ly(
data = dem_data,
x = ~n,
y = ~word,
type = "bar",
orientation = "h",
marker = list(color = "#2980b9")
) %>%
plotly::layout(
title = list(text = "Top Words used by Democrats / Independents", font = list(size = 18)),
xaxis = list(
title = "",
tickfont = list(size = 14)
),
yaxis = list(
title = "",
tickfont = list(size = 14),
categoryorder = "array",
categoryarray = rev(dem_data$word)
),
margin = list(l = 140)
)
dem_plot
Looking at what all members of Congress post on X.com can be informative. But what if you want a detailed look at what one particular member of Congress posted, and about one particular thing?
During the time leading up to the period we are examining, Republican U.S. Rep. Thomas Massie of Kentucky had been speaking and posting about topics that fellow members of his party tended to avoid. His X.com handle is RepThomasMassie. Let’s filter the XData data frame for posts by Massie, then rerun the analysis to see what (just) Massie posted about.
Adding this code between Step 2 and Step 3, then rerunning the code will do the trick:
# ----------------------------------------------------------
# Step 2.5: (Optional filter for a single member)
# ----------------------------------------------------------
XData <- XData %>%
filter(Author == "RepThomasMassie")
Re-running the code gets you a table and two graphics, just like before. But this time, the table and red Republican graphic contain only Massie’s most-posted words. Note that the blue Democratic / independent graphic is blank, because there are no Democrats in the data frame now.
| Party | Word | Mentions |
|---|---|---|
| Rep | war | 33 |
| Rep | act | 30 |
| Rep | bill | 30 |
| Rep | epstein | 27 |
| Rep | congress | 26 |
| Rep | spending | 23 |
| Rep | repthomasmassie | 21 |
| Rep | vote | 21 |
| Rep | files | 19 |
| Rep | debt | 18 |
| Rep | voted | 16 |
| Rep | fbi | 15 |
| Rep | national | 15 |
| Rep | amendment | 14 |
| Rep | government | 14 |
| Rep | house | 14 |
| Rep | support | 14 |
| Rep | constitutional | 13 |
| Rep | farm | 13 |
| Rep | constitution | 12 |
| Rep | meeting | 12 |
| Rep | met | 12 |
Two words in Massie’s X.com vocabulary appear unusually common for a Republican member of Congress: “war,” and “epstein,” the latter referring to convicted sex offender Jeffrey Epstein.
This code will let you type in war and epstein as search terms, then view posts from Massie that mention either term:
# ----------------------------------------------------------
# Step 10: Specify words or phrases to search for in Full.Text
# ----------------------------------------------------------
text_terms <- c(
"war",
"epstein"
)
# Combine into a single regex pattern (match ANY term)
text_pattern <- str_c(text_terms, collapse = "|")
# ----------------------------------------------------------
# Step 11: Filter dataset for matching Full.Text rows
# ----------------------------------------------------------
MatchingPosts <- XData %>%
filter(
str_detect(
str_to_lower(Full.Text),
str_to_lower(text_pattern)
)
) %>%
select(
Date,
Full.Name,
party,
Full.Text
)
# ----------------------------------------------------------
# Step 12: Display matching Full.Text content in a table
# ----------------------------------------------------------
Posts <- MatchingPosts %>%
arrange(Date) %>%
kable(
col.names = c(
"Date",
"Member",
"Party",
"Post Text"
),
caption = "X.com Posts Matching User‑Specified Words or Phrases",
align = c("l", "l", "l", "l")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover"),
full_width = FALSE,
position = "left"
) %>%
scroll_box(
height = "500px",
width = "100%"
)
Posts
| Date | Member | Party | Post Text |
|---|---|---|---|
| 2026-02-28 | RepThomasMassie (Thomas Massie) | Rep | Acts of war unauthorized by Congress. The U.S. is attacking Iran according to AP. https://t.co/Bgwk8yIdRT |
| 2026-02-28 | RepThomasMassie (Thomas Massie) | Rep |
I am opposed to this War. This is not “America First.” When Congress reconvenes, I will work with @RepRoKhanna to force a Congressional vote on war with Iran. The Constitution requires a vote, and your Representative needs to be on record as opposing or supporting this war. |
| 2026-03-01 | RepThomasMassie (Thomas Massie) | Rep | PSA: Bombing a country on the other side of the globe wont make the Epstein files go away, any more than the Dow going above 50,000 will. |
| 2026-03-01 | RepThomasMassie (Thomas Massie) | Rep |
I exposed the global Epstein sex trafficking ring and insisted that Congress debate and authorize any possible war with Iran (to protect our soldiers), and heres the response. Its why so many Congressmen, Republican and Democrat, are afraid to take action on several issues. |
| 2026-03-02 | RepThomasMassie (Thomas Massie) | Rep | @MarindaVannoy1 @DocLanceP If I had left last year, none of the Epstein files would have been released. I think its good to distrust everyone in DC, but what in particular have I done to lose your trust? |
| 2026-03-02 | RepThomasMassie (Thomas Massie) | Rep | @MiDiamondDave @elonmusk Its not that hard to know. These were two of the accounts reposting overseas, urging others to vote for war. https://t.co/7BEDHNIzsV |
| 2026-03-02 | RepThomasMassie (Thomas Massie) | Rep | @SteelrStarsMets @Wovimon @elonmusk The President called it war, and its pretty obvious at this point. |
| 2026-03-02 | RepThomasMassie (Thomas Massie) | Rep |
Investigate Zorro ranch, as well as the men and women at DOJ and FBI who shut this part of the Epstein investigation down. Also, the Epstein Files Transparency Act requires DOJ to release memos and emails detailing their decisions of whether to investigate and/or prosecute. |
| 2026-03-02 | RepThomasMassie (Thomas Massie) | Rep | The administration admits 🇮🇱 dragged us into the 🇮🇷 war thats already cost too many American lives and billions of dollars. Before its over, the price of gas, groceries, and virtually everything else is going to go up. The only winners in 🇺🇸 are defense company shareholders. |
| 2026-03-03 | RepThomasMassie (Thomas Massie) | Rep | @TomHillsisyphus Congress prevented Obama from having an all-out war in Syria in 2013. Surely you didnt think going to war then would have been a good thing? |
| 2026-03-03 | RepThomasMassie (Thomas Massie) | Rep | Theres only one group thats clearly winning in this war… the Military Industrial Complex. Heres my $50 billion theory on why we never have peace, regardless of whos President. @TheoVon @YALiberty https://t.co/8xYuaAcvnC |
| 2026-03-04 | RepThomasMassie (Thomas Massie) | Rep | RT @mandyarthur Your government has raided more raw milk farms than Epstein clients. |
| 2026-03-04 | RepThomasMassie (Thomas Massie) | Rep | For the next two hours, Congress will be debating my War Powers Resolution. Tune in live at this link. Ill be speaking for about five minutes during this debate and will post my speech later today. https://t.co/BE7HSd1zW9 |
| 2026-03-04 | RepThomasMassie (Thomas Massie) | Rep | @MattWalshBlog I think @Rep_Davidson has a new movie project for you… “What is a war?” https://t.co/TEMsHeclQK |
| 2026-03-04 | RepThomasMassie (Thomas Massie) | Rep |
Were debating the Iran War Powers Resolution I co-authored with @RepRoKhanna. Under our Constitution, the power to initiate war rests solely with Congress. Congress owes our service members a clearly defined mission, so that when they accomplish it, they can come home. https://t.co/TBnJH9VJQr |
| 2026-03-05 | RepThomasMassie (Thomas Massie) | Rep | We didnt vote for another War in the Middle East, so why are we getting one? Thank you @SenRandPaul |
| 2026-03-05 | RepThomasMassie (Thomas Massie) | Rep |
Yesterday the House debated my Iran War Powers Resolution for two hours. The VOTE IS TODAY, but weve already won by forcing a debate and a vote. Our troops deserve a clear mission, so when its done they can come home. No more forever wars. Your Rep: https://t.co/MWSufCsG5o https://t.co/MrSRkUedcH |
| 2026-03-05 | RepThomasMassie (Thomas Massie) | Rep | I assume these were approvals in the first week of each of these wars and military invasions. Wars are never as popular as they are on the first day. |
| 2026-03-05 | RepThomasMassie (Thomas Massie) | Rep | RT @WarrenDavidson September of what year? https://t.co/1ZfcTFTVWc |
| 2026-03-05 | RepThomasMassie (Thomas Massie) | Rep |
Yesterday, I spoke with @emilyjashinsky about the Iran War Powers Resolution that @RepRoKhanna and I brought to the floor. Congress must debate and vote—every member will be on the record. The vote will happen today around 4pm. Watch the full interview: https://t.co/nNYCNkQRFF |
| 2026-03-06 | RepThomasMassie (Thomas Massie) | Rep |
Today in @JudiciaryGOP, I voted for a bipartisan amendment to ensure federal officers dont violate American citizens Constitutional rights. I support immigration enforcement and deportation of illegal immigrants, but judicial warrants should be obtained before entering homes. https://t.co/DCSSpPsyY6 |
| 2026-03-06 | RepThomasMassie (Thomas Massie) | Rep | “We are not at war” Orwellian levels of double speak. https://t.co/tdNKdXDK8x |
| 2026-03-06 | RepThomasMassie (Thomas Massie) | Rep |
“We are not at war,” Johnson said. “We have no intention of being at war.” Excuse me, this is war. Even those in favor of it will admit that much. Changing the real meaning of words does not relieve Congress of its Constitutional duty to authorize War. https://t.co/vUApwmJHr6 |
| 2026-03-08 | RepThomasMassie (Thomas Massie) | Rep |
The price of gas has gone up $0.47 and the price of diesel has gone up $0.83 in 10 days due to War with Iran. and waging war costs American taxpayers about $1 billion per day, which comes out to $10 per family per day, or $100 since the war began. This isnt America First. |
| 2026-03-08 | RepThomasMassie (Thomas Massie) | Rep |
@Mrodgerss232 “Look away from the price of gasoline and the cost of groceries and unaffordable housing and the debt and the Epstein files. Focus on something else please!” Say the bots. |
| 2026-03-08 | RepThomasMassie (Thomas Massie) | Rep | @Limare64 Correct. @grok, how much has the price of fertilizer and fertilizer futures increased since the War with Iran began less than two weeks ago? How much could the increased fertilizer prices and fuel prices affect the price of groceries? |
| 2026-03-08 | RepThomasMassie (Thomas Massie) | Rep | @grok @Limare64 What if the war drags on and input prices remain elevated? Will harvest and shipping costs also increase and how will that affect the price of food at grocery stores? |
| 2026-03-08 | RepThomasMassie (Thomas Massie) | Rep | @ensmi99700 @NetworksManager Hey @grok, wasnt I one of the most consistent members of Congress in voting against the war in Ukraine and against funding for the war in Ukraine? |
| 2026-03-08 | RepThomasMassie (Thomas Massie) | Rep |
RT @LouisaClary 🔥Mike Benz on the Epstein bill: “Nobody wanted to be the one to sponsor this bill.” “Nobody wanted to be the Thomas Massie who has their whole career thrown up in the air —getting primaried, hit pieces, and disavowed… and then MTG feeling like she had to drop out of Congress altogether over this.” “…So the mere act of sponsoring the bill put the establishment in a bind. Nobody wanted to be the one to go against it once it was sponsored.” “Everybody feared the base. That once they voted against it, they’d be voted out…” “427-1 it passed in the House. 99-0 in the Senate.” 🔥“This is one of those moments —you could use that same strategy, in theory, not just with the CIA and State Department files on Epstein… [but also] who’s gonna vote against declassification of CIA files for Covid-19?” “Do it fast and furious. I’m inspired by what Thomas Massie has done.” 🇺🇸 —Mike Benz on Kibbe On Liberty Full interview below. |
| 2026-03-09 | RepThomasMassie (Thomas Massie) | Rep | Im saddened to hear that the seventh U.S. military casualty was a brave Kentuckian. My prayers are with all the families of the American service members who have died in the war with Iran, and I am praying for a full recovery of those who have been seriously injured. |
| 2026-03-10 | RepThomasMassie (Thomas Massie) | Rep |
The “Alexander brothers” appeared in the Epstein files by first name, but I noticed DOJ redacted their last name in an FBI email contained in EFTA01660679. But @FBIDirectorKash said no evidence of sex trafficking in the files. https://t.co/475168JuHw |
| 2026-03-10 | RepThomasMassie (Thomas Massie) | Rep | Theyre paying to bus people to the Trump event in my Congressional District. What theyll discover is Trump fans in KY-4 and across the entire Commonwealth also support my work on the Epstein files, reigning in spending, ending forever wars, draining the swamp, and food freedom! https://t.co/rfVcVYf3lh |
| 2026-03-13 | RepThomasMassie (Thomas Massie) | Rep |
RT @takenaps If we had a Congress full of @RepThomasMassie we would have: Borderless Constitutional Carry Reduced National Debt Better Agricultural Policies Healthier Food + Meat Epstein Arrests Medical Freedom Single Issue Bills Voter ID Reduced Welfare State No AIPAC handlers That’s why they hate him. |
| 2026-03-13 | RepThomasMassie (Thomas Massie) | Rep |
just a few of the things I strongly support: ✅The SAVE Act ✅National Constitutional Carry ✅Warrants for Americans for FISA ✅Reduce Spending ✅Convict Epstein Coconspirators ✅Healthy Food and Farm Freedom ✅Abolish the Federal Reserve ✅Border Security ✅Stop Fraud |
| 2026-03-14 | RepThomasMassie (Thomas Massie) | Rep |
The Foreign Intelligence Surveillance Act will expire soon. FBI Directors Mueller, Comey, Wray, and even Patel have used this law to unconstitutionally snoop on Americans without getting a warrant. Its easily fixed if/when reauthorized by Congress. Add 3 words: Get a Warrant! |
| 2026-03-17 | RepThomasMassie (Thomas Massie) | Rep |
The National Kidney Foundation is a voluntary nonprofit that works to raise awareness and fund innovation in kidney disease research. Thank you, Kelly Burbridge, Annie Harrison, and Teresa Villaran from @nkf for meeting with me today. https://t.co/4RdZJlvKBN |
| 2026-03-20 | RepThomasMassie (Thomas Massie) | Rep |
RT @amconmag Rep. Boebert is a NO on $200 billion to fund the Iran War: “I am so tired of spending money elsewhere. Im tired of the Industrial War Complex getting our hard-earned tax dollars. Ive got folks in Colorado who cant afford to live. We need America First policies right now.” https://t.co/EAIJMPC91t |
| 2026-03-22 | RepThomasMassie (Thomas Massie) | Rep | @otter_blues I used to keep bees, so these may be descendants of swarms that escaped into my woods. I also have some neighbors about a mile away, who have been keeping a few hives. |
| 2026-03-24 | RepThomasMassie (Thomas Massie) | Rep | The Epstein Files are an encyclopedia of criminal activity committed by billionaires and politicians across the globe. |
| 2026-03-24 | RepThomasMassie (Thomas Massie) | Rep |
Ill continue to support the valid Constitutional position that Trump, Jordan, and Vance have all expressed strongly in the past: No FISA reauthorization without a warrant requirement for US citizens! https://t.co/FVAXJLFScf |
| 2026-03-24 | RepThomasMassie (Thomas Massie) | Rep | @robertatlee @FmrRepMTG @RepBoebert @RepNancyMace When the party decides to keep the Epstein files hidden, increase spending, reauthorize warrantless spying, and perpetuate the immigrant welfare fraud, I dont “stack hands.” I stick with my constituents and the promises I made to them when I campaigned. |
| 2026-03-25 | RepThomasMassie (Thomas Massie) | Rep | Releasing the Epstein files is not the end goal. Until we see investigations and arrests, our system of justice is not working. |
| 2026-03-27 | RepThomasMassie (Thomas Massie) | Rep |
Many policies from Washington, D.C. these days, like wars abroad, excessive spending, and tariffs are causing a higher cost of living. My PRIME Act, which made it into the Farm Bill, would make it easier for local farms to sell directly to consumers, lowering the price of meat. |
| 2026-03-28 | RepThomasMassie (Thomas Massie) | Rep | @WhiteHouse Can you arrest Epsteins co-conspirators instead of riffing on a porn site ? |
| 2026-03-29 | RepThomasMassie (Thomas Massie) | Rep | Imagine a world where hard work is rewarded, truth and justice prevail in courtrooms, the government doesnt steal your labor by debasing the currency, bureaucrats arent captured by corporations, and our taxes go toward critical infrastructure instead of wars overseas. https://t.co/pO5eSskDkO |
| 2026-03-31 | RepThomasMassie (Thomas Massie) | Rep |
Dan, in your first call, which I think is the first and last occasion you and I ever spoke:
A few hours after the call, I received and released new FBI whistleblower information regarding the all-hands meeting (which matched what you told me in #4 above), related to concern that the meeting was called to “out” the whistelblowers. Your second (attempted) call was the evening I achieved 218 signatures on the Epstein discharge petition and I had been busy thwarting Mike Johnsons last ditch effort to derail the Epstein Files Transparency Act. Not sure why your call log shows 1:36am. You called me in the evening, maybe 8ish? note - my staff also had the unfortunate pleasure of receiving numerous late night calls on Signal from FBI staff telling them there was absolutely nothing in the Epstein case and that I |
| 2026-03-31 | RepThomasMassie (Thomas Massie) | Rep | @dbongino In fact, the only question I asked Pam Bondi at that April 28, 2025 dinner at DOJ, in front of everyone when I was recognized to speak, was “when are you going to release phase 2 of the Epstein files?” There were two dozen people who heard the question. |
| 2026-04-01 | RepThomasMassie (Thomas Massie) | Rep | @dbongino Daniel, you have 5 times as many followers as me, yet I ratioed you three times today. When I caught you in lies, you just started new threads. Have fun interviewing a warm-body fool on April fools day. I think even you pitching softballs will recognize what a goof this guy is. https://t.co/VBaDJBq2dH |
| 2026-04-02 | RepThomasMassie (Thomas Massie) | Rep |
I support Trump firing Pam Bondi. Do you? I hope the next AG will release all the Epstein files according to the law and follow up with investigations, prosecutions, and arrests. |
| 2026-04-02 | RepThomasMassie (Thomas Massie) | Rep |
RT @WayneWaldropW So let me get this straight. Thomas Massie is the man who: - fought to get the Epstein Files released - spoke out against covid mandates - has a near perfect voting record - wants to decrease government spending - supports small farmers - pushes for single issue bills Yet somehow these “Conservative influencers” think HE is the fraudster? Thomas Massie has been consistent, and be is sticking to the mandate you all pretended to support up until last year. Sounds like he is the most America First member of Congress if you ask me! |
| 2026-04-03 | RepThomasMassie (Thomas Massie) | Rep |
RT @KaceeRAllen Rep. Thomas Massie says we wont have justice in America until Jeffrey Epsteins clients get perp-walked in hand cuffs to the jail. “At some point, somebody got to Pam Bondi and said, it’s your job to cover this up.” https://t.co/ofvbgNBTHS |
| 2026-04-03 | RepThomasMassie (Thomas Massie) | Rep | Congratulations AG Blanche. Now you have 30 days to release the rest of the files before becoming criminally liable for failure to comply with the Epstein Files Transparency Act. |
| 2026-04-06 | RepThomasMassie (Thomas Massie) | Rep |
No one should have to beg the government to exercise a constitutionally protected right anywhere in the country. Thank you @RepChuckEdwards for cosponsoring HR 645, the National Constitutional Carry Act. |
| 2026-04-09 | RepThomasMassie (Thomas Massie) | Rep |
First Lady asks Congress to bring Epstein survivors in for testimony. With all due respect, thats @DAGToddBlanches job! @RepRoKhanna & I already gave brave survivors a chance to tell their horrific stories on Capitol Hill. @PamBondi wouldnt even acknowledge them. PROSECUTE! |
| 2026-04-10 | RepThomasMassie (Thomas Massie) | Rep |
I vote with GOP 91% of the time, but thats about to go to 90%. I wont vote to let feds spy on you without a warrant. FISA 702 allows the government to search for your information in vast databases compiled while targeting foreigners. The White House sent me this email today: https://t.co/BW59MlRNvY |
| 2026-04-13 | RepThomasMassie (Thomas Massie) | Rep |
The Epstein class of entitled billionaires and swamp dwellers hate me for bringing transparency, not for obstructing. The Uniparty easily passes bills when Im the sole objector, but I explain whats in the bills & how they violate our Constitution. Its why they want me gone. |
| 2026-04-15 | RepThomasMassie (Thomas Massie) | Rep |
I will be voting NO on final passage of the FISA 702 Reauthorization Bill if it does not include a warrant provision and other reforms to protect US citizens right to privacy. Yesterday I offered these 3 amendments to fix the program, but they were not allowed last night. |
Tennessee’s delegation to the U.S. Congress consists
of two U.S. senators and nine members of the
U.S. House of Representatives. Here are their X.com handles,
their names (in parentheses), and the number of posts by each in the
XData data frame.
Blackburn and Hagerty are the two senators. The rest are members of the U.S. House. All are Republicans except for Cohen, a Democrat who represents the Memphis area:
| Member (as listed in XData) | Number of Posts |
|---|---|
| RepOgles (Rep. Andy Ogles) | 355 |
| MarshaBlackburn (Sen. Marsha Blackburn) | 228 |
| RepDavidKustoff (Rep. David Kustoff) | 109 |
| RepChuck (Chuck Fleischmann) | 97 |
| RepCohen (Steve Cohen) | 73 |
| SenatorHagerty (Senator Bill Hagerty) | 68 |
| RepJohnRose (Congressman John Rose) | 37 |
| DesJarlaisTN04 (Scott DesJarlais) | 33 |
| RepTimBurchett (Rep. Tim Burchett Press Office) | 31 |
Below is the code from today’s lesson, all in one place, including the optional member filter on Step 2.5 and the drill-down code from steps 10 through 12.
Your task is to:
Choose a member of Tennessee’s delegation.
Paste his or her X.com handle into the code
in place of XHandleGoesHere.
Run the code through Step 9 to get a look at the members word table and chart.
Pick an interesting-looking term or two.
Add the term(s) to the text_terms
list on Step 10.
Run steps 10 through 12, and look through the selected posts.
When you can show me your results and give me a quick verbal interpretation of what you found, you are free to leave.
# ----------------------------------------------------------
# Step 1: Install required packages (if missing)
# ----------------------------------------------------------
if (!require("tidyverse")) install.packages("tidyverse")
if (!require("plotly")) install.packages("plotly")
if (!require("tidytext")) install.packages("tidytext")
if (!require("kableExtra")) install.packages("kableExtra")
library(tidyverse)
library(plotly)
library(tidytext)
library(kableExtra)
# ----------------------------------------------------------
# Step 2: Load data
# ----------------------------------------------------------
XData <- read_csv(
"https://github.com/drkblake/Data/raw/refs/heads/main/IranWarCongressX.csv",
col_types = cols(
Date = col_date()
)
)
# ----------------------------------------------------------
# Step 2.5: (Optional filter for a single member)
# ----------------------------------------------------------
XData <- XData %>%
filter(Author == "XHandleGoesHere")
# ----------------------------------------------------------
# Step 3: Tokenize Full.Text into single words
# ----------------------------------------------------------
XData <- XData %>%
mutate(
Full.Text = str_replace_all(Full.Text, "[’‘`]", "")
)
X_words <- XData %>%
unnest_tokens(word, Full.Text)
# ----------------------------------------------------------
# Step 4: Remove stop words
# ----------------------------------------------------------
data("stop_words")
X_words_clean <- X_words %>%
anti_join(stop_words, by = "word")
# ----------------------------------------------------------
# Step 5: Remove X.com–specific noise and contractions
# ----------------------------------------------------------
x_noise <- c(
"https", "tco", "t.co", "rt", "amp",
"co", "com", "www",
"twitter", "x",
"im", "its"
)
X_words_clean <- X_words_clean %>%
filter(
!str_detect(word, "^@"),
!str_detect(word, "^#"),
!word %in% x_noise,
str_detect(word, "[a-z]"),
nchar(word) > 2
)
# ----------------------------------------------------------
# Step 6: Count word usage by party
# ----------------------------------------------------------
party_word_counts <- X_words_clean %>%
count(party, word, sort = TRUE)
# ----------------------------------------------------------
# Step 7: Identify TOP 10 words for each party
# ----------------------------------------------------------
top_words_by_party <- party_word_counts %>%
group_by(party) %>%
slice_max(n, n = 20) %>%
ungroup()
# ----------------------------------------------------------
# Step 8: Display table
# ----------------------------------------------------------
WordTable <- top_words_by_party %>%
arrange(party, desc(n)) %>%
kable(
col.names = c("Party", "Word", "Mentions"),
caption = "Most Common Words in Congressional X Posts by Party (Top 10)"
) %>%
kable_styling(full_width = FALSE)
WordTable
# ----------------------------------------------------------
# Step 9: Plotly visualization (larger labels, top-down order)
# ----------------------------------------------------------
rep_data <- top_words_by_party %>%
filter(party == "Rep") %>%
arrange(desc(n))
dem_data <- top_words_by_party %>%
filter(party == "Dem/Ind") %>%
arrange(desc(n))
rep_plot <- plot_ly(
data = rep_data,
x = ~n,
y = ~word,
type = "bar",
orientation = "h",
marker = list(color = "#c0392b")
) %>%
plotly::layout(
title = list(text = "Top Words for Republicans", font = list(size = 18)),
xaxis = list(
title = "",
tickfont = list(size = 14)
),
yaxis = list(
title = "",
tickfont = list(size = 14),
categoryorder = "array",
categoryarray = rev(rep_data$word)
),
margin = list(l = 140)
)
rep_plot
dem_plot <- plot_ly(
data = dem_data,
x = ~n,
y = ~word,
type = "bar",
orientation = "h",
marker = list(color = "#2980b9")
) %>%
plotly::layout(
title = list(text = "Top Words for Democrats / Independents", font = list(size = 18)),
xaxis = list(
title = "",
tickfont = list(size = 14)
),
yaxis = list(
title = "",
tickfont = list(size = 14),
categoryorder = "array",
categoryarray = rev(dem_data$word)
),
margin = list(l = 140)
)
dem_plot
# ----------------------------------------------------------
# Step 10: Specify words or phrases to search for in Full.Text
# ----------------------------------------------------------
text_terms <- c(
"war",
"epstein"
)
# Combine into a single regex pattern (match ANY term)
text_pattern <- str_c(text_terms, collapse = "|")
# ----------------------------------------------------------
# Step 11: Filter dataset for matching Full.Text rows
# ----------------------------------------------------------
MatchingPosts <- XData %>%
filter(
str_detect(
str_to_lower(Full.Text),
str_to_lower(text_pattern)
)
) %>%
select(
Date,
Full.Name,
party,
Full.Text
)
# ----------------------------------------------------------
# Step 12: Display matching Full.Text content in a table
# ----------------------------------------------------------
Posts <- MatchingPosts %>%
arrange(Date) %>%
kable(
col.names = c(
"Date",
"Member",
"Party",
"Post Text"
),
caption = "X.com Posts Matching User‑Specified Words or Phrases",
align = c("l", "l", "l", "l")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover"),
full_width = FALSE,
position = "left"
) %>%
scroll_box(
height = "500px",
width = "100%"
)
Posts