Banned Book Persistence Project:¶
Do historically persistent banned books continue to be targeted in the modern wave of school bans, and where is that activity concentrated?¶
By Anjelica Martinez
About the Project and Data
I started this project out of curiosity over the nature of book bans and whether there were any patterns in the type of books that are banned. Are they specific titles that persist over decades? Do the bans focus around subject matter and themes?
I was hoping to review as much data as possible stretching back to the 1900s but unfortunately there is only data for the past 3 decades (1990s, 2000s, 2010s) since the American Library Association's (ALA): Office for Intellectual Freedom, a non-profit organization based in the United States that tracks and reports on book challenges and censorship attempts in U.S. schools and libraries, only began documenting book bans in 1990. The organization gathers its data from confidential reporting from library professionals, community members, and nationally published news stories. Hence, it only shows a snapshot of the book bans that are happening across the nation's libraries.
import pandas as pd
import numpy as np
import geopandas as gpd
import matplotlib.pyplot as plt
import seaborn as sns
PATH = "../data/ala_challenged_books.csv"
df = pd.read_csv(PATH)
df.head()
| title | alt-title | author(s) | decade | source | notes | |
|---|---|---|---|---|---|---|
| 0 | The Absolutely True Diary of a Part-Time Indian | NaN | Sherman Alexie | 2010s | ALA_OIF | NaN |
| 1 | Captain Underpants | NaN | Dav Pilkey | 2010s | ALA_OIF | series |
| 2 | Thirteen Reasons Why | NaN | Jay Asher | 2010s | ALA_OIF | NaN |
| 3 | Looking for Alaska | NaN | John Green | 2010s | ALA_OIF | NaN |
| 4 | Melissa | George | Alex Gino | 2010s | ALA_OIF | Title renamed in later editions |
df.info()
df.head(10)
<class 'pandas.core.frame.DataFrame'> RangeIndex: 30 entries, 0 to 29 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 title 30 non-null object 1 alt-title 1 non-null object 2 author(s) 30 non-null object 3 decade 30 non-null object 4 source 30 non-null object 5 notes 9 non-null object dtypes: object(6) memory usage: 1.5+ KB
| title | alt-title | author(s) | decade | source | notes | |
|---|---|---|---|---|---|---|
| 0 | The Absolutely True Diary of a Part-Time Indian | NaN | Sherman Alexie | 2010s | ALA_OIF | NaN |
| 1 | Captain Underpants | NaN | Dav Pilkey | 2010s | ALA_OIF | series |
| 2 | Thirteen Reasons Why | NaN | Jay Asher | 2010s | ALA_OIF | NaN |
| 3 | Looking for Alaska | NaN | John Green | 2010s | ALA_OIF | NaN |
| 4 | Melissa | George | Alex Gino | 2010s | ALA_OIF | Title renamed in later editions |
| 5 | And Tango Makes Three | NaN | Justin Richardson and Peter Parnell | 2010s | ALA_OIF | NaN |
| 6 | Drama | NaN | Raina Telgemeier | 2010s | ALA_OIF | NaN |
| 7 | Fifty Shades of Grey | NaN | E. L. James | 2010s | ALA_OIF | NaN |
| 8 | Internet Girls | NaN | Lauren Myracle | 2010s | ALA_OIF | series |
| 9 | The Bluest Eye | NaN | Toni Morrison | 2010s | ALA_OIF | NaN |
print("Total rows:", len(df))
print("Unique titles:", df["title"].unique())
print("Decades", sorted(df["decade"].unique()))
Total rows: 30 Unique titles: ['The Absolutely True Diary of a Part-Time Indian' 'Captain Underpants' 'Thirteen Reasons Why ' 'Looking for Alaska' 'Melissa' 'And Tango Makes Three' 'Drama' 'Fifty Shades of Grey' 'Internet Girls' 'The Bluest Eye' 'Harry Potter' 'Alice series ' 'The Chocolate War' 'Of Mice and Men' 'I Know Why the Caged Bird Sings' 'Scary Stories' 'His Dark Materials' 'ttyl; ttfn; l8r g8r' 'The Perks of Being a Wallflower' 'Daddy’s Roommate' 'The Adventures of Huckleberry Finn' 'Forever' 'Bridge to Terabithia' 'Heather Has Two Mommies' 'The Catcher in the Rye'] Decades ['1990s', '2000s', '2010s']
persistence = (
df.groupby("title", as_index=False)
.agg(
n_decades=("decade", "nunique"),
decades=("decade", lambda s: ", ".join(sorted(set(s)))),
author=("author(s)", lambda s: s.dropna().iloc[0])
)
.sort_values(["n_decades", "title"], ascending=[False, True])
)
persistence
| title | n_decades | decades | author | |
|---|---|---|---|---|
| 1 | And Tango Makes Three | 2 | 2000s, 2010s | Justin Richardson and Peter Parnell |
| 11 | I Know Why the Caged Bird Sings | 2 | 1990s, 2000s | Maya Angelou |
| 15 | Of Mice and Men | 2 | 1990s, 2000s | John Steinbeck |
| 16 | Scary Stories | 2 | 1990s, 2000s | Alvin Schwartz |
| 21 | The Chocolate War | 2 | 1990s, 2000s | Robert Cormier |
| 0 | Alice series | 1 | 2000s | Phyllis Reynolds Naylor |
| 2 | Bridge to Terabithia | 1 | 1990s | Katherine Paterson |
| 3 | Captain Underpants | 1 | 2010s | Dav Pilkey |
| 4 | Daddy’s Roommate | 1 | 1990s | Michael Willhoite |
| 5 | Drama | 1 | 2010s | Raina Telgemeier |
| 6 | Fifty Shades of Grey | 1 | 2010s | E. L. James |
| 7 | Forever | 1 | 1990s | Judy Blume |
| 8 | Harry Potter | 1 | 2000s | J.K. Rowling |
| 9 | Heather Has Two Mommies | 1 | 1990s | Leslea Newman |
| 10 | His Dark Materials | 1 | 2000s | Philip Pullman |
| 12 | Internet Girls | 1 | 2010s | Lauren Myracle |
| 13 | Looking for Alaska | 1 | 2010s | John Green |
| 14 | Melissa | 1 | 2010s | Alex Gino |
| 17 | The Absolutely True Diary of a Part-Time Indian | 1 | 2010s | Sherman Alexie |
| 18 | The Adventures of Huckleberry Finn | 1 | 1990s | Mark Twain |
| 19 | The Bluest Eye | 1 | 2010s | Toni Morrison |
| 20 | The Catcher in the Rye | 1 | 1990s | J.D. Salinger |
| 22 | The Perks of Being a Wallflower | 1 | 2000s | Stephen Chbosky |
| 23 | Thirteen Reasons Why | 1 | 2010s | Jay Asher |
| 24 | ttyl; ttfn; l8r g8r | 1 | 2000s | Lauren Myracle |
persistence.head()
| title | n_decades | decades | author | |
|---|---|---|---|---|
| 1 | And Tango Makes Three | 2 | 2000s, 2010s | Justin Richardson and Peter Parnell |
| 11 | I Know Why the Caged Bird Sings | 2 | 1990s, 2000s | Maya Angelou |
| 15 | Of Mice and Men | 2 | 1990s, 2000s | John Steinbeck |
| 16 | Scary Stories | 2 | 1990s, 2000s | Alvin Schwartz |
| 21 | The Chocolate War | 2 | 1990s, 2000s | Robert Cormier |
Part 1: Which book(s) keeps showing up across decades?¶
The ALA compiles yearly lists of the top 10 banned books. To make my analysis more concise, I focused on the decades data that compiles the top 100 books of that particular decade (i.e. 1990s). From each decade I took the top 10 banned books and then put together all three decades into one dataset. From there, I tallied up the books to find which books came up the most on the lists. This landed me at the following books:
Most persistent books across decades
And Tango makes three by Justin Richardson and Peter Parnell
I Know Why the Caged Bird Sings by Maya Angelou
Of Mice and Men by John Steinback
Scary Stories by Alvin Schwartz
The Chocolate War by Robert Cormier
These five book titles appear across two decades, while the remaining only appear in one decade. It could mean that the books being challenged are a specific small group of titles rather than a rotation of books.
persistent_books = persistence[persistence["n_decades"] > 1].copy()
persistent_books
| title | n_decades | decades | author | |
|---|---|---|---|---|
| 1 | And Tango Makes Three | 2 | 2000s, 2010s | Justin Richardson and Peter Parnell |
| 11 | I Know Why the Caged Bird Sings | 2 | 1990s, 2000s | Maya Angelou |
| 15 | Of Mice and Men | 2 | 1990s, 2000s | John Steinbeck |
| 16 | Scary Stories | 2 | 1990s, 2000s | Alvin Schwartz |
| 21 | The Chocolate War | 2 | 1990s, 2000s | Robert Cormier |
persistent_books[["title", "author", "n_decades", "decades"]]
| title | author | n_decades | decades | |
|---|---|---|---|---|
| 1 | And Tango Makes Three | Justin Richardson and Peter Parnell | 2 | 2000s, 2010s |
| 11 | I Know Why the Caged Bird Sings | Maya Angelou | 2 | 1990s, 2000s |
| 15 | Of Mice and Men | John Steinbeck | 2 | 1990s, 2000s |
| 16 | Scary Stories | Alvin Schwartz | 2 | 1990s, 2000s |
| 21 | The Chocolate War | Robert Cormier | 2 | 1990s, 2000s |
persistent_books["theme"] = [
"LGBTQ+ representation",
"Race, autobiography",
"Violence, classic literature",
"Violence, fear",
"Authority, sexuality"]
persistent_books["theme_source"] = ("ALA challenge descriptions and publisher summaries")
persistent_books
| title | n_decades | decades | author | theme | theme_source | |
|---|---|---|---|---|---|---|
| 1 | And Tango Makes Three | 2 | 2000s, 2010s | Justin Richardson and Peter Parnell | LGBTQ+ representation | ALA challenge descriptions and publisher summa... |
| 11 | I Know Why the Caged Bird Sings | 2 | 1990s, 2000s | Maya Angelou | Race, autobiography | ALA challenge descriptions and publisher summa... |
| 15 | Of Mice and Men | 2 | 1990s, 2000s | John Steinbeck | Violence, classic literature | ALA challenge descriptions and publisher summa... |
| 16 | Scary Stories | 2 | 1990s, 2000s | Alvin Schwartz | Violence, fear | ALA challenge descriptions and publisher summa... |
| 21 | The Chocolate War | 2 | 1990s, 2000s | Robert Cormier | Authority, sexuality | ALA challenge descriptions and publisher summa... |
Part 2: Theme/Topic Patterns¶
When we look at the book themes we see a pattern taking place. For these 5 books that are repeated twice within the 90s, 2000s, and 2010s decades the themes are:
Banned Book Themes
- LGTBQ+ representation
- And Tango Makes Three:
- Race and Marginalization:
- I Know Why The Caged Bird Sings:
- Youth perspective, involve violence, fear, or challenges to authority:
- Of Mice and Men
- Scary Stories
- The Chocolate War
Now that we know the top 5 books that persist across decades, I wanted to find out of these books, how many times do they appear in the 2020s specifically.
persistent_books.to_csv(
"../data/persistent_books_themes.csv",
index=False
)
Which books persist across decades, and where are those same books being banned during the contemporary surge in bans (2021–2025)?
pen_2122 = pd.read_csv("../data/pen_book_bans_2021-2022.csv")
pen_2223 = pd.read_csv("../data/pen_book_bans_2022-2023.csv")
pen_2324 = pd.read_csv("../data/pen_book_bans_2023-2024.csv")
pen_2425 = pd.read_csv("../data/pen_book_bans_2024-2025.csv")
# Adds a source period
pen_2122["reporting_period"] = "2021-2022"
pen_2223["reporting_period"] = "2022-2023"
pen_2324["reporting_period"] = "2023-2024"
pen_2425["reporting_period"] = "2024-2025"
pen = pd.concat(
[pen_2122, pen_2223, pen_2324, pen_2425],
ignore_index=True
)
pen.info()
pen["reporting_period"].value_counts()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 22659 entries, 0 to 22658 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Author 22658 non-null object 1 Title 22658 non-null object 2 Type of Ban 2532 non-null object 3 Secondary Author(s) 1287 non-null object 4 Illustrator(s) 1724 non-null object 5 Translator(s) 211 non-null object 6 State 22659 non-null object 7 District 22659 non-null object 8 Date of Challenge/Removal 12613 non-null object 9 Origin of Challenge 5894 non-null object 10 reporting_period 22659 non-null object 11 Series Name 4138 non-null object 12 Ban Status 20127 non-null object 13 Challenge/Removal 10046 non-null object 14 Initiating Action 10046 non-null object 15 Series 2205 non-null object dtypes: object(16) memory usage: 2.8+ MB
reporting_period 2023-2024 10046 2024-2025 6719 2022-2023 3362 2021-2022 2532 Name: count, dtype: int64
pen["Title"].value_counts().head(10)
Title Looking for Alaska 147 Nineteen Minutes 142 Sold 136 The Perks of Being a Wallflower 135 Thirteen Reasons Why 126 Crank 116 The Bluest Eye 116 The Kite Runner 115 Tricks 112 Identical 110 Name: count, dtype: int64
pen["State"].value_counts().head(10)
State Florida 8837 Iowa 3798 Texas 3745 Tennessee 2016 Pennsylvania 737 Nation 590 Wisconsin 482 Missouri 419 Utah 351 Virginia 312 Name: count, dtype: int64
target_titles = persistent_books["title"].tolist()
pen_persistent = pen[pen["Title"].isin(target_titles)].copy()
len(pen_persistent)
43
pen_persistent["Title"].value_counts()
Title And Tango Makes Three 20 The Chocolate War 10 I Know Why the Caged Bird Sings 8 Of Mice and Men 5 Name: count, dtype: int64
ban_counts_2020s = (
pen_persistent["Title"]
.value_counts()
.reset_index()
)
ban_counts_2020s.columns = ["Book Title", "Number of Bans (2021-2025)"]
ban_counts_2020s
| Book Title | Number of Bans (2021-2025) | |
|---|---|---|
| 0 | And Tango Makes Three | 20 |
| 1 | The Chocolate War | 10 |
| 2 | I Know Why the Caged Bird Sings | 8 |
| 3 | Of Mice and Men | 5 |
When we take our top 5 books that are seen more than twice in the three decades, we find that one of the books, Scary Stories to Tell in the Dark is missing from the line up. This could be related to society being more concerned with other topics that challenge social values or identity rather than books, like Scary Stories, being too scary for children to read vs books that challenge social values and identity.
Top 4 Persistent Banned Books in 2021-2025
- And Tango Makes Three
- The Chocolate War
- I Know Why the Caged Bird Sings
- Of Mice and Men
To better understand why these books consistently appear in the banned books list I dove into what each book is about and what themes are seen in the book and link to the reasons they were banned and what that says about our society.
And Tango Makes Three is a children's book about a same-sex family of penguins. The controversy here is that it challenges the norm of a heterosexual couples and shows at early childhood that same-sex families exist. It reflects the continued discomfort that LGTBQ+ is being normalized and that it's ok.
The Chocolate War is a book that is anti-authority, anti-conformity and anti-moral-compliance and resists the idea that youth literature should reinforce instritutional authority. For schools that value discipline, hierarchy and rule-following this causes a disruptive message.
I Know Why The Caged Bird Sings this book forces confrontation with system racism and sexual abuse, taboo topics that are continously labelled as inappropriate. It shows a discomfort with the teaching and understanding of racial trauma.
Of Mice and Men shows how harsh and unfair society can be instead of telling a hopeful story about things getting better. Many find this topic uncomfortable and would rather not expose students to this type of material.
As we can see, each of these books challenges core norms around family, authority, race and identity that school institutions find threatening in modern times.
pen_persistent["State"].value_counts()
State Florida 19 Iowa 13 Tennessee 4 Texas 3 Oklahoma 1 Pennsylvania 1 Wisconsin 1 North Carolina 1 Name: count, dtype: int64
Florida (19 bans) and Iowa (13 bans) are the states with the most persistent book bans (2021-2025). When we think of book bans we think of them being a national phenemenon but it looks like it's actually only a few states that are pushing this. Why these states? You would think it might be because there are more people that are objecting over certain titles but after some digging I discovered it's actually because these states have policies in place that make it easier to ban books and easier to spread across school systems.
state_counts = (
pen_persistent["State"]
.value_counts()
.reset_index()
)
state_counts.columns = ["state", "ban_count"]
state_counts
| state | ban_count | |
|---|---|---|
| 0 | Florida | 19 |
| 1 | Iowa | 13 |
| 2 | Tennessee | 4 |
| 3 | Texas | 3 |
| 4 | Oklahoma | 1 |
| 5 | Pennsylvania | 1 |
| 6 | Wisconsin | 1 |
| 7 | North Carolina | 1 |
# Load state geometry
states = gpd.read_file("https://www2.census.gov/geo/tiger/GENZ2021/shp/cb_2021_us_state_20m.zip")
# Keep contiguous US only
states = states[~states["NAME"].isin(
[ "Alaska", "Hawaii", "Puerto Rico"]
)]
states[["NAME", "geometry"]].head()
| NAME | geometry | |
|---|---|---|
| 0 | Louisiana | POLYGON ((-94.04305 32.69303, -94.04303 32.797... |
| 2 | Maryland | MULTIPOLYGON (((-76.04621 38.02553, -76.00734 ... |
| 3 | Wisconsin | MULTIPOLYGON (((-86.93428 45.42115, -86.83575 ... |
| 4 | Florida | MULTIPOLYGON (((-81.81169 24.56874, -81.75127 ... |
| 5 | Georgia | POLYGON ((-85.60516 34.98468, -85.47434 34.983... |
geo = states.merge(
state_counts,
left_on="NAME",
right_on="state",
how="left"
)
geo["ban_count"] = geo["ban_count"].fillna(0)
geo[["NAME", "ban_count"]].sort_values("ban_count", ascending=False).head(10)
| NAME | ban_count | |
|---|---|---|
| 3 | Florida | 19.0 |
| 7 | Iowa | 13.0 |
| 5 | Tennessee | 4.0 |
| 13 | Texas | 3.0 |
| 2 | Wisconsin | 1.0 |
| 35 | Pennsylvania | 1.0 |
| 36 | Oklahoma | 1.0 |
| 37 | North Carolina | 1.0 |
| 38 | Massachusetts | 0.0 |
| 29 | Kansas | 0.0 |
fig, ax = plt.subplots(1, 1, figsize=(12, 8))
geo.plot(
column="ban_count",
ax=ax,
legend=True,
cmap="Blues",
vmin=0,
vmax=20, # or np.percentile(geo["ban_count"], 95)
edgecolor="black",
linewidth=0.3
)
ax.set_title("Modern bans of historically persistent books by state (PEN America)",
fontsize=14)
ax.axis("off")
plt.tight_layout()
plt.show()
Which titles drive bans in high-activity states?
Florida leads with the most bans of the And Tango Makes Three (LGTBQ+ representative book), whereas Iowa has the highest bans with the book The Chocolate War (anti-authority), Florida also has the highest bans of I Know Why The Caged Bird Sings (systemic racism and sexual abuse).
It appears that different regions target different specific themes. This could further imply that book bans are shaped by local priorities rather than a single national agenda.
# Creates table with the count of banned books by state
state_title_counts = (
pen_persistent
.groupby(["State", "Title"], as_index=False)
.size()
.rename(columns={"size":"ban_count"})
)
# Sorts table in ascending order
state_title_counts.sort_values("ban_count", ascending=False)
| State | Title | ban_count | |
|---|---|---|---|
| 0 | Florida | And Tango Makes Three | 12 |
| 7 | Iowa | The Chocolate War | 6 |
| 4 | Iowa | And Tango Makes Three | 4 |
| 3 | Florida | The Chocolate War | 3 |
| 1 | Florida | I Know Why the Caged Bird Sings | 3 |
| 12 | Tennessee | I Know Why the Caged Bird Sings | 2 |
| 5 | Iowa | I Know Why the Caged Bird Sings | 2 |
| 16 | Texas | Of Mice and Men | 1 |
| 15 | Texas | I Know Why the Caged Bird Sings | 1 |
| 14 | Texas | And Tango Makes Three | 1 |
| 13 | Tennessee | Of Mice and Men | 1 |
| 9 | Oklahoma | Of Mice and Men | 1 |
| 11 | Tennessee | And Tango Makes Three | 1 |
| 10 | Pennsylvania | And Tango Makes Three | 1 |
| 8 | North Carolina | And Tango Makes Three | 1 |
| 6 | Iowa | Of Mice and Men | 1 |
| 2 | Florida | Of Mice and Men | 1 |
| 17 | Wisconsin | The Chocolate War | 1 |
top_states = ["Florida", "Iowa", "Tennessee", "Texas"]
plot_data = state_title_counts[
state_title_counts["State"].isin(top_states)
]
plt.figure(figsize=(10, 6))
sns.barplot(
data=plot_data,
x="State",
y="ban_count",
hue="Title",
palette="Set2"
)
plt.title("Bans of Historically Persistent Books by State (2021-2025)")
plt.ylabel("Number of times banned")
plt.xlabel("State")
plt.legend(title="Book Title", bbox_to_anchor=(1.05, 1), loc="upper left")
plt.tight_layout()
plt.show()