Technical writing
NLRB Elections and Labor Enforcement Data: The Federal Database Behind Union Organizing and Unfair Labor Practice Cases
The National Labor Relations Board maintains two parallel federal databases that together constitute the authoritative public record of union organizing activity and labor law enforcement in the United States private sector: a representation election database covering every NLRB-supervised election conducted since the 1930s, and an Unfair Labor Practice case database tracking charges filed by employees, unions, and employers for violations of the National Labor Relations Act. These datasets document the full arc of American collective bargaining — from petition filing through election results, certification, first-contract bargaining, and enforcement proceedings — and they are available to the public without registration or access fees.
What the NLRB databases are
The NLRB administers two distinct categories of cases under the National Labor Relations Act. “R cases” — representation cases — involve petitions by unions or employees seeking NLRB-supervised elections to determine whether workers want union representation. “C cases” — charge cases — involve allegations that an employer or union has committed an unfair labor practice in violation of the NLRA. Each category generates a structured dataset that the NLRB publishes through a combination of downloadable CSV files, a JSON case search API, interactive dashboards, and annual statistical reports.
The election database is the more quantitatively tractable of the two. The NLRB publishes an annual CSV file for each fiscal year (October through September) covering every election conducted in that year. Each row encodes a single election: the case number, petition filing date, election date, NLRB regional office number, union name, employer name, six-digit NAICS industry code, the number of eligible voters in the bargaining unit, votes cast for and against union representation, and the outcome. These structured fields make it possible to compute union win rates by year, industry, region, and bargaining unit size; to measure petition-to-election lead times (a direct measure of the impact of successive rule changes); and to identify organizing surges like the 2021–2023 Amazon and Starbucks waves in the underlying data.
The ULP case database is accessible primarily through the NLRB case search interface and JSON API at nlrb.gov, which exposes individual case records including case type, filing date, regional office, employer and union names, current status, and case close method. Individual case files — the charge forms, investigation reports, ALJ decisions, Board orders, and court enforcement petitions — are available through the NLRB's electronic case management system and FOIA requests for closed cases. The NLRB receives approximately 15,000 to 20,000 ULP charges per year across all charge types.
| Database | Format | Update frequency | Coverage |
|---|---|---|---|
| Election results CSV | CSV (annual file per FY) | Annually after FY close | FY2000–present; earlier via Annual Reports |
| NLRB Case API | JSON (paginated) | Near-real-time | All open and recently closed R and C cases |
| NLRB Decisions database | PDF, HTML | Ongoing | Published Board decisions, ALJ decisions |
| NLRB Data Dashboard | Interactive web charts | Periodic | Petition volume, win rates, ULP filings by region |
| NLRB Annual Report | PDF report | Annually | Comprehensive statistics since 1940s |
The representation election process
The NLRA's framework for union organizing is organized around the secret-ballot election administered by the NLRB. Section 7 of the NLRA guarantees employees the right to organize, to form, join, or assist labor organizations, to bargain collectively through representatives of their own choosing, and to engage in concerted activities for mutual aid and protection. It also guarantees the right to refrain from any of these activities. Section 9 of the NLRA establishes the mechanics of how workers exercise the Section 7 right to choose a bargaining representative: through an election supervised by the NLRB in which a majority of valid votes cast (not a majority of all eligible voters) determines the outcome.
The election process begins with a petition. The three primary petition types are: RC (Representation, Certification of Representative), filed by a union or group of employees seeking certification as exclusive bargaining agent for an appropriate unit — the most common type; RD (Decertification), filed by employees seeking to remove a currently certified or recognized union, requiring a 30% showing of interest from unit employees; and RM (Representation, Employer Petition), filed by an employer with a good-faith, reasonable basis to doubt a recognized union's continued majority support. RC petitions must be accompanied by a showing of interest — authorization cards or employee signatures — from at least 30% of the proposed unit, though experienced organizers typically seek 60–70% card support before filing to preserve a margin during the pre-election campaign.
After a petition is filed with the appropriate NLRB regional office, the Regional Director investigates the proposed bargaining unit under the “community of interest” standard. This standard assesses the similarity of wages and working conditions among proposed unit members, the degree of common supervision, physical proximity, the history of collective bargaining at the employer, and employee desires. The parties may reach a Stipulated Election Agreement specifying unit composition and election logistics, or the Regional Director may order a pre-election hearing. After the unit is determined, the Director issues a Direction of Election and the NLRB sets an election date.
The timeline between petition filing and election date has been the most contested operational variable in NLRB election administration. The 2014 Obama-era “Ambush Election” rule reduced median pre-election periods from approximately 38 days to approximately 23 days by streamlining pre-hearing procedures and accelerating the employer's obligation to provide a voter list (including home addresses, telephone numbers, and personal email addresses if known) within two business days of a Direction of Election or Stipulated Agreement. The first Trump administration rescinded this rule in 2019, reinstating longer timelines. The 2023 Biden-era rule restored the 2014 framework and added further procedural reforms including the blocking charge procedure, which allows a pending ULP charge alleging employer misconduct to suspend an election while the charge is investigated.
The election itself is conducted by secret ballot, typically at the employer's premises, by mail for geographically dispersed units, or by a combination depending on circumstances including the COVID-19 adaptations that became common after 2020. A union that wins a majority of valid votes cast is certified as the exclusive bargaining representative for the unit. Certification triggers the employer's legal obligation under Section 8(a)(5) to bargain in good faith over wages, hours, and other terms and conditions of employment. If the union loses, an election bar prevents any new election petition in the same unit for 12 months. A certified union that wins its election is protected by a certification bar for 12 months (and frequently longer under case law), during which decertification petitions are barred.
Election petition data
At the peak of NLRB election activity in the mid-1970s, the Board processed more than 8,000 election petitions per year. Annual petition volume declined steadily over the following four decades, reaching a trough of approximately 1,500 to 2,000 petitions per year through the 2010s. This long-run decline reflected the structural shift of American employment away from large integrated manufacturing facilities — where union organizing had historically concentrated — toward smaller service-sector workplaces with higher turnover, greater geographic dispersion, and fewer historical union footholds.
Union win rates track the share of decided elections (elections resulting in either a certified win or a union loss) in which the union received a majority of valid votes cast. Through the 2010s, win rates in RC elections hovered between 55% and 65% across all industries combined — a historically elevated win rate compared to the 1970s and 1980s, when unions won fewer than half of contested elections. The improvement reflects a strategic shift by labor organizations toward smaller, more carefully selected organizing targets where card support was already high before petition filing, reducing the share of losing campaigns in the overall win-rate calculation.
| Period | Est. annual petitions | Union win rate | Key context |
|---|---|---|---|
| Mid-1970s (peak) | 8,000+ | ~48% | Peak organizing era; large manufacturing units |
| Early 2000s | ~2,400 | ~56% | Smaller unit targeting; service-sector shift |
| FY2016–FY2020 | ~1,600–2,100 | ~62% | 2014 rule in effect FY2015–FY2019; pandemic FY2020 |
| FY2022 (surge) | ~2,510 RC petitions | ~67% | Amazon, Starbucks wave; 57% YoY increase in petitions |
| FY2023 | ~2,700+ | ~68% | 2023 rule issued August; Starbucks at 400+ petitions |
Bargaining unit size has declined dramatically over the same period. Median unit sizes in election petitions filed in recent fiscal years are approximately 20 to 30 eligible voters, compared to units of 100 or more that characterized manufacturing campaigns in the 1970s. This reflects the organizing environment: service-sector workplaces, retail stores, coffee shops, and fulfillment center shifts generate small-unit elections by the nature of their employment structures. Small-unit elections are individually less consequential for aggregate union density — a win in a 25-person unit contributes less to total union membership than a win in a 500-person unit — but they allow organizing committees to build wins in a shorter timeframe and with less capital investment in an organizing campaign. The Amazon LDJ5 election (approximately 8,300 eligible voters) was anomalously large relative to the current election environment and received disproportionate coverage in part because of its unusual scale.
ULP case structure
Unfair Labor Practice cases are designated as “C cases” in NLRB nomenclature and are identified by a case number that encodes the regional office (two-digit prefix) and the case type (two-letter suffix). The most common C case types are:
| Case type | Who is charged | Primary NLRA sections involved |
|---|---|---|
| CA — Charge Against Employer | Employer | Sections 8(a)(1)–8(a)(5) |
| CB — Charge Against Union | Labor organization | Sections 8(b)(1)–8(b)(7) |
| CC — Secondary Boycott | Labor organization | Section 8(b)(4) |
| CD — Jurisdictional Dispute | Labor organization | Section 8(b)(4)(D) |
| CE — Breach of Contract | Labor organization | Section 301, NLRA Section 8(e) |
The CA case — charge against an employer — is by far the most common ULP filing type, accounting for approximately 70–75% of all ULP charges in a typical year. Any individual, organization, or employer may file a ULP charge with the NLRB regional office at no cost, using NLRB Form 501. The charge must be filed within six months of the date the charging party knew or should have known of the alleged violation — a statute of limitations strictly enforced by the Board.
After a charge is filed, a Regional Office investigator takes sworn affidavits from witnesses, requests documents, and assesses the evidence. If the investigation supports the charge, the Regional Director issues a formal complaint. If not, the Director issues a dismissal letter, which the charging party may appeal to the NLRB Office of Appeals in Washington. Approximately 30–35% of charges that are not withdrawn are found to have merit. Of charges found meritorious, a significant proportion are resolved through informal settlement (the employer agrees to a remedy without formal proceedings) before a complaint issues. The NLRB's overall settlement rate across ULP case types runs approximately 85%, reflecting the Board's strong preference for voluntary resolution over contested litigation.
When a complaint issues and informal settlement fails, the case proceeds to a formal hearing before an Administrative Law Judge. The ALJ conducts a trial-type evidentiary proceeding, receives witness testimony subject to cross-examination, and issues a written decision with factual findings and legal conclusions. Either party may file exceptions with the full Board. A final Board order finding a violation typically includes: a cease-and-desist directive; a notice-posting requirement (requiring the employer to post an official NLRB notice of employee rights in the workplace); reinstatement of unlawfully discharged employees with back pay, less interim earnings; and other specific relief tailored to the violation. Board orders require court enforcement — the NLRB petitions the US Court of Appeals to enter a judgment enforcing the Board order, after which contempt proceedings are available for non-compliance.
Major recent cases: Amazon, Starbucks, UAW, Writers Guild
The April 2022 election at Amazon's LDJ5 fulfillment center in Staten Island, New York produced the first successful union election at any Amazon facility in the United States. The Amazon Labor Union, an independent union unaffiliated with any national labor federation, was organized primarily by warehouse workers who had worked at LDJ5 during the COVID-19 pandemic. The election, conducted over two days on April 1 and 2, 2022, covered approximately 8,300 eligible voters. The final vote count was 2,654 for the ALU to 2,131 against, a margin of 523 votes. Amazon filed numerous election objections alleging NLRB regional staff misconduct and improper union conduct during the campaign; the Regional Director overruled the objections. A subsequent NLRB election at the nearby LDJ4 facility was won by Amazon.
Post-certification, the ALU and Amazon have not reached a collective bargaining agreement. Amazon refused initial bargaining overtures; the NLRB General Counsel's office issued ULP complaints alleging Amazon's failure to bargain in good faith under Section 8(a)(5). The case exemplifies a structural challenge in the NLRB election system: certification confers a legal obligation to bargain, but the NLRA has no mechanism to compel a contract if the parties cannot reach agreement. Unions in the United States negotiate their first contracts after certification in an environment where employers have strong incentives to delay, and first-contract failure rates are substantial — studies suggest that 30 to 40% of newly certified bargaining units never reach a first contract.
The Starbucks Workers United campaign, launched in December 2021 at a Buffalo, New York location, generated the largest sustained single-employer organizing wave in the NLRB system since the 1970s. By the end of 2023, more than 400 Starbucks locations had filed RC election petitions, and unions had won elections at a substantial majority of stores where ballots were cast. Starbucks simultaneously ran one of the most documented anti-union employer campaigns in recent NLRB history: the NLRB General Counsel issued hundreds of ULP complaints against Starbucks, alleging violations including the termination of union organizers in violation of Section 8(a)(3), the withholding of wage increases and benefit improvements from unionizing stores that were simultaneously provided to non-unionizing stores in violation of Section 8(a)(3) and 8(a)(5), the closure of stores following successful union elections in violation of Section 8(a)(3), and coercive interrogation of employees about union activities in violation of Section 8(a)(1).
A federal district court granted a Section 10(j) injunction in one consolidated Starbucks proceeding, ordering reinstatement of terminated organizers on an interim basis while the ULP complaints proceeded before the Board — one of the most significant uses of 10(j) injunctive relief in a retail organizing context in recent decades. In late 2023 and 2024, Starbucks and Starbucks Workers United reached a framework agreement to begin negotiating contracts at unionized stores, representing a significant shift in the employer's posture after years of refusing to engage substantively at the bargaining table.
The United Auto Workers launched a historic organizing campaign in 2023 targeting non-union automotive plants operated by foreign manufacturers in the American South — including Volkswagen's Chattanooga, Tennessee assembly plant and several Mercedes-Benz facilities in Alabama. The UAW had previously filed RC petitions at the Volkswagen Chattanooga plant in 2014 and 2019, losing both elections after state-level political figures publicly opposed unionization and the company declined to remain neutral. The 2023 campaign followed the UAW's successful Stand Up Strike against the Detroit Three automakers (Ford, General Motors, and Stellantis), which produced record contract improvements and gave the UAW a public profile that organizers credited with increasing interest at foreign-owned plants. The April 2024 election at the Volkswagen Chattanooga plant resulted in a decisive UAW victory — 2,628 for the UAW to 985 against — the first successful union election at a foreign-owned southern automobile plant in American history.
The entertainment industry's 2023 labor disputes demonstrated the NLRB's role in industries that already have high union density but face novel bargaining disputes over emerging technology. The Writers Guild of America West (WGA) strike against the Alliance of Motion Picture and Television Producers, which ran from May through September 2023, and the subsequent Screen Actors Guild- AFTRA (SAG-AFTRA) strike, involved extensive NLRB ULP charge filings by both sides. The WGA strike generated ULP charges alleging producer refusal to bargain in good faith over artificial intelligence provisions — specifically, whether studios could use AI to generate or revise scripts in ways that reduced the need for WGA-covered writers. These AI bargaining disputes represent a novel category of Section 8(a)(5) and Section 8(b)(3) good-faith bargaining claims that the NLRB system has not previously adjudicated at scale.
Section 8 violation categories
Section 8 of the NLRA defines unfair labor practices for both employers (Section 8(a)) and labor organizations (Section 8(b)). Understanding the subsection structure is essential for interpreting ULP case data, because NLRB case records and Annual Report statistics are organized by these subsection categories.
Employer ULP subsections under Section 8(a):
| Subsection | Prohibition | Common violations |
|---|---|---|
| 8(a)(1) | Interference with, restraint, or coercion of employees in the exercise of Section 7 rights | Threatening statements, interrogating employees about union activity, unlawful surveillance, overbroad workplace rules restricting protected concerted activity, polling employees about union support |
| 8(a)(2) | Domination or unlawful financial support of a labor organization | Assisting a company union, providing resources to a union the employer prefers over a competing union, dominating employee committee structures that function as labor organizations |
| 8(a)(3) | Discrimination in hire, tenure, or conditions of employment to encourage or discourage union membership | Discharging or demoting union organizers, closing facilities following union election wins, withholding wage increases from unionizing locations, assigning undesirable shifts to union activists |
| 8(a)(4) | Discrimination against employees for filing NLRB charges or testifying in Board proceedings | Retaliating against workers who cooperated with NLRB investigations or provided affidavits in charge proceedings |
| 8(a)(5) | Refusal to bargain in good faith with the exclusive representative of the employer's employees | Refusing to meet with the union, making unilateral changes to wages or working conditions without bargaining, failing to provide relevant information requested by the union, surface bargaining (going through the motions without intent to reach agreement) |
Section 8(a)(1) is the broadest of the employer ULP provisions and is almost always alleged alongside more specific subsections: every 8(a)(3) discriminatory discharge also violates 8(a)(1) because the discharge inherently interferes with the discharged worker's Section 7 rights and chills the exercise of those rights by remaining employees. In NLRB annual statistics, the count of 8(a)(1) allegations substantially exceeds the number of distinct ULP cases because it is alleged as a derivative theory in virtually every CA case, regardless of the primary basis.
The 8(a)(3) discriminatory discharge is the most heavily litigated individual employer ULP and the one most directly relevant to organizing campaign dynamics. When the NLRB finds an 8(a)(3) violation, the standard remedy is reinstatement and back pay — the employee is restored to their former position and receives the wages they would have earned from the date of unlawful discharge to the date of the Board order, less interim earnings from other employment (the “make-whole” remedy). Back pay under the NLRA traditionally has not included compensation for non-economic harms, and interest on back pay awards has been computed at the federal short-term rate rather than a market rate. In 2022, the NLRB General Counsel announced a policy of pursuing consequential damages including compensation for search costs, credit damage, and other economic harms consequential to unlawful discharge — an expansion of the make-whole remedy that, if sustained by the courts, would substantially increase the cost of 8(a)(3) violations.
Union ULP subsections under Section 8(b):
| Subsection | Prohibition and common violations |
|---|---|
| 8(b)(1)(A) | Restraint or coercion of employees in exercise of Section 7 rights; includes the right not to join a union. Common violations: mass picketing that physically blocks entry, threats against non-striking employees, breach of duty of fair representation in grievance processing. |
| 8(b)(2) | Causing or attempting to cause an employer to discriminate against employees in violation of Section 8(a)(3). Common: union-security clause overreach, closed-shop arrangements (illegal since Taft-Hartley), excessive hiring hall referral discrimination. |
| 8(b)(3) | Refusal to bargain in good faith. Common: union-side surface bargaining, insisting on illegal contract terms, refusing to provide financial information in wage proposals. |
| 8(b)(4) | Secondary boycott activity: inducing employees to refuse to handle goods, strike, or otherwise pressure neutral (secondary) employers to force the primary employer to recognize the union or accede to bargaining demands. One of the most litigated union ULP provisions. |
| 8(b)(5) | Excessive or discriminatory initiation fees in union-security agreements. Rarely litigated in practice; standard initiation fees at most unions fall within accepted ranges. |
| 8(b)(7) | Recognitional picketing where a competing union is already certified, where a valid election was held within the past 12 months, or where an election petition is not filed within 30 days of picketing commencing. |
Union ULP charges (CB cases) constitute approximately 25 to 30% of total ULP filings in a typical year. The vast majority of CB cases involve 8(b)(1)(A) allegations, including the duty of fair representation — the obligation that flows from a union's status as exclusive bargaining representative to represent all unit members, including non-members, without hostility, bad faith, or arbitrary conduct. Fair representation charges surge after contract negotiations that result in concessions or after the union declines to process a member's grievance. These cases are among the most time-consuming in the NLRB system because they often require examination of internal union deliberative processes and the reasonableness of the union's judgments in individual grievance handling.
Data access and the NLRB API
The NLRB provides several access points for case data. The most structured are the annual election results CSV files and the JSON case search API.
Election results CSV files are available at nlrb.gov/reports/graphs-data/recent-election-results. A separate file exists for each fiscal year. The filename follows the patternnlrb-election-report-fyYYYY.csv, updated with a one-to-two fiscal-quarter lag after the fiscal year closes. These files contain one row per election and include all the fields needed for win-rate and lead-time analysis: case number, petition filing date, election date, case close date, regional office number, union name, employer name, six-digit NAICS code, industry description, election type (RC/RM/RD), bargaining unit size, votes for and against, and outcome. The files require no API key or registration and are freely downloadable via HTTP.
The NLRB case search API at https://www.nlrb.gov/api/casesis a JSON endpoint that accepts query parameters for case type, regional office, filing year, state, and free-text search, and returns paginated results. Key query parameters include:
| Parameter | Type | Description |
|---|---|---|
f_case_type | string | Case type code: RC, RD, RM, CA, CB, CC, CD, CE |
f_region | string | Two-digit NLRB regional office number (e.g. “13” for Chicago) |
f_filing_year | string | Four-digit calendar year of case filing |
f_state_code | string | Two-letter state abbreviation (e.g. “NY”, “CA”, “TX”) |
q | string | Free-text search across employer name, union name, and case notes |
page | integer | Page number (1-indexed); results are paginated |
per_page | integer | Results per page; maximum varies (typically 25 or 100) |
The API response includes a count field with the total number of matching records and a results array with case objects. Each case object includes the case number, case type, filing date, NLRB region, employer name, union name, current status, close method, and a link to the case detail page. No API key is required; the endpoint is a public NLRB web service. Rate limiting is not formally documented but polite usage (0.5–1 second between requests) avoids server-side throttling. The endpoint returns data for both open and recently closed cases; historical case data from closed cases further in the past may require FOIA requests or access to the NLRB Annual Reports.
The NLRB Decisions database at nlrb.gov/cases-decisions/nlrb-decisions provides published Board decisions as searchable PDF and HTML documents, organized by date and searchable by party name, keyword, and section number. The NLRB's weekly “Summary of NLRB Decisions” digest is an email service providing brief descriptions of significant new Board decisions, available by subscription at the NLRB website. For researchers tracking doctrinal developments, the weekly digest is a practical monitoring tool.
The NLRB's Annual Report, published each year covering the prior fiscal year, provides aggregate statistics on all aspects of NLRB operations: total case filings by type and region, complaint issuance rates, settlement rates, election results broken down by union affiliation and industry, average processing times, back pay awarded, and regional office performance metrics. Annual Reports are available at nlrb.gov/reports going back to the 1940s and constitute the primary longitudinal source for long-run trend analysis. For research requiring data before FY2000 (where the election CSV files begin), the Annual Reports are the main available structured source.
Python workflow
The following script demonstrates how to combine the NLRB election results CSV, the NLRB case JSON API, and BLS union density data into a single analytical workflow. The script performs three tasks: (1) downloads and parses the NLRB annual election results CSV, computing win rates by fiscal year, industry supersector, and union — and calculating median petition-to-election lead times by regional office as a measure of election rule impact; (2) queries the NLRB case API for CA (charge against employer) cases by state and filing year, computing a disposition summary; and (3) prints BLS private-sector union density data by industry for contextual cross-referencing. No API key is required. The only non-standard library dependencies are requests (optional substitute for urllib), csv, io, and collections from the Python standard library.
For multi-year analysis, concatenate the annual CSV files from nlrb.gov/reports/graphs-data/recent-election-results. Files are available back to FY2000 and use consistent column structures after minor normalization. The fiscal year suffix in the filename (e.g.fy2024) increments each October; update the URLELECTION_CSV_URL constant after the NLRB publishes the new file for the most recent completed fiscal year.
import csv
import io
import json
import time
from collections import Counter, defaultdict
from datetime import datetime, date
from urllib.request import urlopen, Request
# ---------------------------------------------------------------------------
# NLRB Elections and ULP Case Analysis
#
# The NLRB publishes two structured datasets:
#
# 1. Election results CSV (annual, per fiscal year):
# https://www.nlrb.gov/reports/graphs-data/recent-election-results
# Columns include: case_number, date_filed, election_date, date_closed,
# region, union_name, employer_name, naics_code, naics_description,
# election_type (RC/RM/RD), unit_size, votes_for, votes_against, outcome
#
# 2. NLRB Case Search API (JSON):
# https://www.nlrb.gov/api/cases
# Query parameters:
# f_case_type - RC, RD, RM, CA, CB, CC, CD
# f_region - two-digit region number (e.g. "13")
# f_filing_year - four-digit year
# f_state_code - two-letter state abbreviation
# q - free-text search term
# page - page number (1-indexed)
# per_page - results per page (max 25 or 100 depending on endpoint)
#
# No API key required. Data is freely accessible under FOIA.
# ---------------------------------------------------------------------------
# ---- Configuration --------------------------------------------------------
ELECTION_CSV_URL = (
"https://www.nlrb.gov/sites/default/files/attachments/pages/"
"node-174/nlrb-election-report-fy2024.csv"
)
CASE_API_BASE = "https://www.nlrb.gov/api/cases"
# BLS Union Membership data (CPS Outgoing Rotation Group, published annually).
# Source: BLS News Release USDL-24-0072 (January 2024), Table 3.
# Private-sector union density by major industry (2023 annual averages).
BLS_UNION_DENSITY_2023 = {
"Agriculture and related": 1.3,
"Mining": 7.3,
"Construction": 12.6,
"Manufacturing": 8.0,
"Wholesale and retail trade": 4.5,
"Transportation and warehousing": 16.0,
"Utilities": 22.0,
"Information": 7.6,
"Financial activities": 1.6,
"Professional and business svcs": 2.1,
"Education and health services": 8.9,
"Leisure and hospitality": 1.6,
"Other services": 2.5,
}
# NAICS 2-digit prefix -> broad supersector grouping for election data
NAICS_SUPERSECTORS = {
"11": "Agriculture/Forestry",
"21": "Mining/Oil & Gas",
"22": "Utilities",
"23": "Construction",
"31": "Manufacturing", "32": "Manufacturing", "33": "Manufacturing",
"42": "Wholesale Trade",
"44": "Retail Trade", "45": "Retail Trade",
"48": "Transportation/Warehousing", "49": "Transportation/Warehousing",
"51": "Information",
"52": "Finance/Insurance",
"53": "Real Estate",
"54": "Professional Services",
"55": "Management",
"56": "Administrative Services",
"61": "Education",
"62": "Healthcare/Social Assist",
"71": "Arts/Entertainment",
"72": "Accommodation/Food Service",
"81": "Other Services",
"92": "Public Administration",
}
REGION_NAMES = {
"1": "Boston", "2": "New York City",
"3": "Buffalo", "4": "Philadelphia",
"5": "Baltimore", "6": "Pittsburgh",
"7": "Detroit", "8": "Cleveland",
"9": "Cincinnati", "10": "Atlanta",
"11": "Winston-Salem", "12": "Tampa",
"13": "Chicago", "14": "St. Louis",
"15": "New Orleans", "16": "Fort Worth",
"17": "Kansas City", "18": "Minneapolis",
"19": "Seattle", "20": "San Francisco",
"21": "Los Angeles", "22": "Newark",
"24": "Hato Rey (PR)", "25": "Indianapolis",
"26": "Memphis", "28": "Phoenix",
"29": "Brooklyn", "30": "Milwaukee",
"31": "Los Angeles II", "32": "Oakland",
"34": "Hartford",
}
# ---- Utility functions ---------------------------------------------------
def fetch_csv(url: str, timeout: int = 60) -> list[dict]:
"""Download an NLRB election results CSV; return rows as list of dicts."""
req = Request(url, headers={"User-Agent": "labor-research/1.0"})
with urlopen(req, timeout=timeout) as resp:
raw = resp.read().decode("utf-8-sig") # strip UTF-8 BOM if present
return list(csv.DictReader(io.StringIO(raw)))
def fetch_json_api(params: dict, timeout: int = 30) -> dict:
"""Query the NLRB case JSON API with the given parameters."""
qs = "&".join(f"${k}=${v}" for k, v in params.items())
url = f"${CASE_API_BASE}?${qs}"
req = Request(url, headers={
"User-Agent": "labor-research/1.0",
"Accept": "application/json",
})
with urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
def parse_date(s: str) -> date | None:
"""Parse M/D/YYYY or YYYY-MM-DD date strings; return None on failure."""
s = (s or "").strip()
for fmt in ("%m/%d/%Y", "%Y-%m-%d", "%m/%d/%y"):
try:
return datetime.strptime(s, fmt).date()
except ValueError:
pass
return None
def naics_supersector(naics: str) -> str:
return NAICS_SUPERSECTORS.get((naics or "")[:2], "Unknown")
def days_between(d1: date | None, d2: date | None) -> int | None:
if d1 and d2:
return abs((d2 - d1).days)
return None
def safe_int(s: str, default: int = 0) -> int:
try:
return int((s or str(default)).replace(",", ""))
except ValueError:
return default
# ---- Part 1: Election results analysis -----------------------------------
def load_elections(rows: list[dict]) -> list[dict]:
"""Normalize raw CSV rows into structured election records."""
elections = []
for row in rows:
r = {
k.strip().lower().replace(" ", "_"): (v or "").strip()
for k, v in row.items()
}
etype = r.get("election_type", "").upper()
if etype not in ("RC", "RM", "RD"):
continue
filed = parse_date(r.get("date_filed", ""))
closed = parse_date(r.get("date_closed", ""))
elec = parse_date(r.get("election_date", ""))
outcome = r.get("outcome", "").upper()
votes_for = safe_int(r.get("votes_for", "0"))
votes_against = safe_int(r.get("votes_against", "0"))
unit_size = safe_int(r.get("unit_size", "0"))
# Determine win: explicit outcome field or vote majority
won = (outcome == "WON") or (
outcome not in ("WON", "LOST") and votes_for > votes_against > 0
)
elections.append({
"case_number": r.get("case_number", ""),
"etype": etype,
"year": filed.year if filed else None,
"month": filed.month if filed else None,
"filed": filed,
"elec": elec,
"closed": closed,
"outcome": outcome,
"won": won,
"decided": outcome in ("WON", "LOST"),
"union": r.get("union_name", "Unknown"),
"employer": r.get("employer_name", ""),
"naics": r.get("naics_code", ""),
"supersector": naics_supersector(r.get("naics_code", "")),
"region": r.get("region", "").strip(),
"unit_size": unit_size,
"votes_for": votes_for,
"votes_against": votes_against,
"lead_days": days_between(filed, elec),
})
return elections
def analyze_elections(elections: list[dict]) -> None:
decided = [e for e in elections if e["decided"]]
# --- Table 1: Win rate by fiscal year ---
by_year: dict[int, list] = defaultdict(list)
for e in decided:
if e["year"]:
by_year[e["year"]].append(e)
print("\nNLRB Election Win Rates by Fiscal Year (RC/RM/RD elections held)")
print("=" * 72)
print(f" {'Year':<6} {'Elections':>10} {'Won':>6} {'Win%':>7} "
f"{'Avg Unit':>9} {'Med Unit':>9}")
print(" " + "-" * 70)
for year in sorted(by_year.keys()):
es = by_year[year]
won = sum(1 for e in es if e["won"])
sizes = sorted(e["unit_size"] for e in es if e["unit_size"] > 0)
avg = sum(sizes) / len(sizes) if sizes else 0
med = sizes[len(sizes) // 2] if sizes else 0
pct = 100 * won / len(es) if es else 0
print(f" {year:<6} {len(es):>10,} {won:>6,} {pct:>6.1f}% "
f"{avg:>9.0f} {med:>9}")
# --- Table 2: Win rate by NAICS supersector ---
by_sector: dict[str, list] = defaultdict(list)
for e in decided:
by_sector[e["supersector"]].append(e)
print("\nElection Win Rates by NAICS Industry Supersector")
print("=" * 58)
print(f" {'Sector':<28} {'Elections':>10} {'Won':>6} {'Win%':>7}")
print(" " + "-" * 56)
ranked = sorted(
by_sector.items(),
key=lambda x: -(100 * sum(1 for e in x[1] if e["won"]) / len(x[1]))
if x[1] else 0
)
for sector, es in ranked:
won = sum(1 for e in es if e["won"])
pct = 100 * won / len(es) if es else 0
print(f" {sector:<28} {len(es):>10,} {won:>6,} {pct:>6.1f}%")
# --- Table 3: Top 15 unions by petition volume and win rate ---
by_union: dict[str, list] = defaultdict(list)
for e in decided:
name = (e["union"] or "Unknown")[:36]
by_union[name].append(e)
print("\nTop 15 Unions by Election Volume (decided elections)")
print("=" * 64)
print(f" {'Union':<36} {'Elections':>10} {'Won':>6} {'Win%':>7}")
print(" " + "-" * 62)
top = sorted(by_union.items(), key=lambda x: -len(x[1]))[:15]
for union, es in top:
won = sum(1 for e in es if e["won"])
pct = 100 * won / len(es) if es else 0
print(f" {union:<36} {len(es):>10,} {won:>6,} {pct:>6.1f}%")
# --- Table 4: Petition-to-election lead time by region ---
lead_by_region: dict[str, list[int]] = defaultdict(list)
for e in elections:
if e["lead_days"] and 5 < e["lead_days"] < 365 and e["region"]:
lead_by_region[e["region"]].append(e["lead_days"])
print("\nMedian Petition-to-Election Days by Regional Office")
print("=" * 55)
print(f" {'Rgn':<5} {'Office':<22} {'Cases':>7} {'Median':>8} {'Mean':>8}")
print(" " + "-" * 53)
ranked_regions = sorted(
lead_by_region.items(),
key=lambda x: sorted(x[1])[len(x[1]) // 2]
)
for region, days_list in ranked_regions:
srt = sorted(days_list)
med = srt[len(srt) // 2]
avg = sum(srt) / len(srt)
name = REGION_NAMES.get(region, f"Region {region}")
print(f" {region:<5} {name:<22} {len(srt):>7,} {med:>8} {avg:>8.1f}")
# ---- Part 2: ULP CA case search via NLRB JSON API -----------------------
def fetch_ulp_ca_cases(
state: str = "NY",
year: str = "2023",
max_pages: int = 5,
) -> list[dict]:
"""
Retrieve CA (Unfair Labor Practice - Employer) cases from the NLRB API
for a given state and filing year. Returns a list of case dicts.
The NLRB case search API returns JSON with a 'results' list.
Each result includes: case_number, case_type, filing_date, region,
employer_name, union_name, status, close_method, and related fields.
"""
all_cases: list[dict] = []
for page in range(1, max_pages + 1):
try:
data = fetch_json_api({
"f_case_type": "CA",
"f_state_code": state,
"f_filing_year": year,
"page": str(page),
"per_page": "25",
})
except Exception as exc:
print(f" [API page {page}] error: {exc}")
break
results = data.get("results", [])
if not results:
break
all_cases.extend(results)
total = data.get("count", 0)
print(f" Page {page}: retrieved {len(results)} cases "
f"(total available: {total:,})")
if len(all_cases) >= total:
break
time.sleep(0.5) # polite rate limiting
return all_cases
def summarize_ulp_cases(cases: list[dict]) -> None:
"""Print a summary of ULP CA case dispositions and common close methods."""
by_status: Counter = Counter()
by_close: Counter = Counter()
for case in cases:
status = (case.get("status") or "Unknown").strip()
close = (case.get("close_method") or "Open/Pending").strip()
by_status[status] += 1
by_close[close] += 1
print("\n ULP CA Case Status Distribution")
print(" " + "-" * 40)
for status, count in by_status.most_common():
pct = 100 * count / len(cases) if cases else 0
print(f" {status:<28} {count:>5,} ({pct:.1f}%)")
print("\n ULP CA Case Close Method Distribution")
print(" " + "-" * 45)
for method, count in by_close.most_common(10):
pct = 100 * count / len(cases) if cases else 0
print(f" {method:<34} {count:>5,} ({pct:.1f}%)")
# ---- Part 3: BLS union density cross-reference --------------------------
def print_bls_density_context() -> None:
"""Print BLS private-sector union density by industry for context."""
print("\nBLS Private-Sector Union Density by Industry, 2023 Annual Average")
print("Source: BLS News Release USDL-24-0072, Table 3 (CPS-ORG data)")
print("=" * 60)
print(f" {'Industry':<40} {'Density %':>10}")
print(" " + "-" * 58)
for industry, density in sorted(
BLS_UNION_DENSITY_2023.items(), key=lambda x: -x[1]
):
bar = "#" * int(density)
print(f" {industry:<40} {density:>9.1f}% {bar}")
print()
total_private = 6.0
print(f" {'Overall private sector':<40} {total_private:>9.1f}%")
print(f" {'Overall public sector':<40} 33.1%")
print(f" {'All wage & salary workers':<40} 10.0%")
# ---- Main ---------------------------------------------------------------
def main() -> None:
# Part 1: Election results
print("Downloading NLRB election results CSV...")
try:
raw_rows = fetch_csv(ELECTION_CSV_URL)
print(f" Downloaded {len(raw_rows):,} rows.")
elections = load_elections(raw_rows)
print(f" Parsed {len(elections):,} RC/RM/RD election records.")
analyze_elections(elections)
except Exception as exc:
print(f" CSV download failed: {exc}")
print(" Check nlrb.gov/reports/graphs-data/recent-election-results")
print(" for the current fiscal year filename.")
# Part 2: ULP case search (NY state, 2023)
print("\nQuerying NLRB API for CA cases (NY, FY2023)...")
try:
ulp_cases = fetch_ulp_ca_cases(state="NY", year="2023", max_pages=5)
print(f" Retrieved {len(ulp_cases):,} CA cases.")
if ulp_cases:
summarize_ulp_cases(ulp_cases)
except Exception as exc:
print(f" API query failed: {exc}")
print(" Endpoint: https://www.nlrb.gov/api/cases")
# Part 3: BLS density context
print_bls_density_context()
if __name__ == "__main__":
main()
The election lead-time analysis by regional office (Table 4 in the script output) directly quantifies the effect of election rule changes. Regional offices that processed elections in under 20 days are typically those with lighter dockets, fewer contested unit determinations, and more Stipulated Election Agreements relative to litigated Directions of Election. The regional variance in lead times is substantial even under the 2023 rule framework, meaning that the geographic location of an organizing campaign materially affects how much campaign time elapses before employees vote.
The BLS union density cross-reference illustrates the inverse relationship between existing union density and organizing campaign frequency in a sector. Accommodation and food service (1.6% union density in 2023) and retail trade (approximately 4.5%) generate disproportionately high shares of NLRB election petitions relative to their workforce size, because these are sectors where unions are attempting to establish footholds in previously non-union territory. Transportation and warehousing (16.0%) and utilities (22.0%) generate fewer elections per covered worker because union density is already substantial and election activity is concentrated in smaller geographical units at the margins of existing bargaining relationships.
Analytical limitations
The NLRB election results database measures formal organizing activity channeled through the NLRB petition process, not organizing activity overall. Card-check recognition — in which an employer voluntarily recognizes a union based on a showing of authorization card majority without a Board-supervised election — does not appear in the NLRB election database, though it may be reflected in UC or AC case filings. The prevalence of voluntary recognition varies significantly by industry and employer; in some heavily unionized industries (construction, entertainment, healthcare in some regions), voluntary recognition agreements account for a substantial share of new bargaining relationships.
Win rates computed from the NLRB election database measure wins among elections that were actually held, not wins among all petitions filed. A substantial fraction of RC petitions — historically 20 to 30% — are withdrawn before an election is held, typically because card support eroded during the campaign, the parties reached a settlement, or the union determined the unit was not appropriate. Withdrawn petitions are not included in win-rate calculations. Including withdrawals as “losses” in a broader organizing effectiveness measure would substantially reduce the apparent win rate.
ULP charge disposition statistics must be interpreted carefully. The approximately 35% merit rate (charges found meritorious by the Regional Director) reflects the Regional Director's assessment based on available evidence and legal standards, not a determination that the remaining 65% of charges were fabricated or baseless. Some charges are dismissed because witnesses are unavailable, evidence is not preserved, the statute of limitations runs, or the relevant conduct falls within an employer right (such as permanently replacing economic strikers) that the Board has recognized as lawful. The NLRB's evidentiary standards for charge investigation do not require a preponderance of evidence to find merit — a reasonable factual basis is sufficient — meaning that the 35% merit rate likely understates the fraction of charges that involve genuine employer misconduct that could be proven under a higher evidentiary standard.
The NLRB case API does not provide a bulk download of the complete case database in a single operation; researchers must paginate through results in batches of 25 to 100 per page. For large-scale research covering all CA cases filed in a given year (which may number 10,000 or more at the national level), iterating through the full API result set requires 200 to 400 HTTP requests, which should be made with appropriate rate limiting to avoid server-side throttling. The NLRB Annual Report provides aggregate statistics that are often more efficient for national-level trend analysis than API pagination.
Finally, NLRB election outcomes and ULP charge filings are lagging indicators of labor market dynamics. The surge in organizing activity visible in FY2022 and FY2023 petition data reflects conditions and sentiments that developed over the preceding 18–24 months: the pandemic labor market disruptions of 2020–2021, the heightened public visibility of essential worker conditions during COVID-19, the tight labor market of 2021–2022, and the demonstrator effect of high-profile wins at Amazon and Starbucks on organizing committees at other employers. The relationship between economic conditions and organizing activity is real but operates on a meaningful lag and through multiple intermediate mechanisms, making NLRB petition data a retrospective rather than a real-time measure of worker sentiment.