How to Join FAA Aircraft Records to Their Make and Model
On this page
Problem statement
The FAA registry's main file says what an aircraft is only as a seven-character code, MFR MDL CODE. Manufacturer, model, seats, weight class and engine count are in a second file, ACFTREF.txt, and the join between them is straightforward, but the names it returns are not: one manufacturer appears under several spellings, and one model family under many model strings. This page joins the two with the 16 October 2026 files, checks the join, and turns model strings into families and seat counts.
Quick answer
Read both with faa() so the codes keep their leading zeros, and merge with a many-to-one check:
v = reg[reg["STATUS CODE"] == "V"]
m = v.merge(ref, left_on="MFR MDL CODE", right_on="CODE", how="left", validate="many_to_one", indicator=True)
print(m._merge.value_counts().to_string())
print(m.groupby(["MFR", "MODEL"]).size().nlargest(6).to_string())
_merge
both 310844
left_only 0
right_only 0
MFR MODEL
PIPER PA-28-140 4155
CIRRUS DESIGN CORP SR22 3927
CESSNA 172M 3534
PIPER PA-28-180 3451
J3C-65 3149
CESSNA 172N 3098
All 310,844 valid registrations find their code in ACFTREF, and validate="many_to_one" confirms each code appears there once. The most common single model string is the Piper PA-28-140, then the Cirrus SR22 and Cessna 172M.
The code, the names and the families
1. What the seven characters hold
print(ref[["CODE", "MFR", "MODEL", "NO-SEATS", "AC-WEIGHT"]].sample(4, random_state=3).to_string(index=False))
print(f"codes: {ref.CODE.nunique():,} unique of {len(ref):,} rows; manufacturer part (first 3): {ref.CODE.str[:3].nunique():,}")
CODE MFR MODEL NO-SEATS AC-WEIGHT
05676LB DROUIN REJEAN J PANTHER 2 PLUS 002 CLASS 1
0560301 HOLMES MILTON N EAGLE 002 CLASS 1
060002M AVIADESIGN INC A16 002 CLASS 1
05648XR KAISER HOWARD R KR 2 002 CLASS 1
codes: 94,115 unique of 94,115 rows; manufacturer part (first 3): 559
ACFTREF has 94,115 codes, one row each. The first three characters identify the manufacturer, 559 of them, and the rest the model; amateur builders get their own codes, which is why individuals' names appear as manufacturers. NO-SEATS is text with leading zeros, so convert it before comparing.
2. One maker, several names
cess = m[m.MFR.str.contains("CESSNA", na=False)]
print(cess.MFR.value_counts().to_string())
MFR
CESSNA 71734
CESSNA AIRCRAFT CO 381
CESSNA ECTOR 20
CESSNA/AIR REPAIR INC 8
CESSNA WREN 4
CESSNA CLAASSEN 2
CESSNA ALLARDICE 1
CESSNA/RAVEN 1
CESSNA CONROY 1
CESSNA-MACONE 1
CESSNA/INLAND VALLEY AVIA 1
CESSNA-MORSE 1
CESSNA-LOOMIS-BUEHN 1
CESSNA-VANSANT INTERPRISES-INC 1
CESSNA/WHITEHOUSE 1
CESSNA/WEAVER 1
Cessna's 71,734 valid aircraft are under CESSNA, but 381 more are under CESSNA AIRCRAFT CO and a tail of modified or rebuilt aircraft under names such as CESSNA/AIR REPAIR INC. Grouping on the exact string splits the manufacturer; match on a prefix and decide case by case about the rebuilds.
3. Model families, not model strings
m["family"] = m.MODEL.str.extract(r"^(\d{3}|[A-Z]+-?\d+)", expand=False)
c172 = m[m.MFR.str.contains("CESSNA", na=False) & (m.family == "172")]
print(f"Cessna 172 rows: {len(c172):,} across {c172.MODEL.nunique()} model strings; top: {c172.MODEL.value_counts().head(4).to_dict()}")
Cessna 172 rows: 19,191 across 19 model strings; top: {'172M': 3534, '172N': 3098, '172S': 1909, '172': 1777}
The Cessna 172, the most-produced light aircraft, is 19,191 registrations across 19 model strings, led by the 172M, 172N, 172S and plain 172. As one family it outnumbers the Piper PA-28-140 more than four times.
Seats and weight classes
The data used on this page
The FAA's Releasable Aircraft Database, public domain, downloaded on 16 October 2026 and saved on this site, because the FAA replaces it every day. The saved copy leaves out the owners' names and street addresses and keeps five-digit ZIP codes; every other column is as published. With it come the Census Bureau's Vintage 2025 county population estimates, 2024 county boundaries and 2024 ZCTA gazetteer.
from pathlib import Path
import zipfile
import geopandas as gpd
import numpy as np
import pandas as pd
import requests
Path("data").mkdir(exist_ok=True)
# FAA Releasable Aircraft Database, 16 October 2026: MASTER.txt without the NAME, STREET, STREET2 and OTHER NAMES
# columns and with five-digit ZIP codes (MASTER-no-names.txt), ACFTREF.txt and ENGINE.txt as published; Census
# Vintage 2025 county estimates, 2024 cartographic boundary counties and 2024 ZCTA gazetteer. Saved here unchanged
# otherwise, because the FAA replaces the file daily.
# Live: https://registry.faa.gov/database/ReleasableAircraft.zip (needs a browser User-Agent),
# https://www2.census.gov/geo/docs/maps-data/data/gazetteer/2024_Gazetteer/2024_Gaz_zcta_national.zip
ARCHIVE = "faa-aircraft-registry-2026-10-16.zip"
if not Path(f"data/{ARCHIVE}").exists():
Path(f"data/{ARCHIVE}").write_bytes(requests.get(f"https://data.spatialworkflow.io/{ARCHIVE}", timeout=600).content)
Z = zipfile.ZipFile(f"data/{ARCHIVE}")
def faa(name):
"""One registry file: comma-separated, every field padded with spaces, a byte-order mark and a trailing comma."""
d = pd.read_csv(Z.open(name), dtype=str, encoding="utf-8-sig", keep_default_na=False)
d.columns = d.columns.str.strip()
d = d.loc[:, [c for c in d.columns if c and not c.startswith("Unnamed")]]
return d.apply(lambda c: c.str.strip())
reg = faa("MASTER-no-names.txt")
ref = faa("ACFTREF.txt")
TYPE_AIRCRAFT = {"1": "glider", "2": "balloon", "3": "blimp", "4": "fixed wing single engine", "5": "fixed wing multi engine",
"6": "rotorcraft", "7": "weight-shift", "8": "powered parachute", "9": "gyroplane", "H": "hybrid lift", "O": "other"}
REGISTRANT = {"1": "individual", "2": "partnership", "3": "corporation", "4": "co-owned", "5": "government", "7": "LLC",
"8": "non-citizen corporation", "9": "non-citizen co-owned", "": "blank"}
Path("data/cb_2024_us_county_20m.zip").write_bytes(Z.read("cb_2024_us_county_20m.zip"))
counties = gpd.read_file("data/cb_2024_us_county_20m.zip")
STATEFP = counties.drop_duplicates("STUSPS").set_index("STUSPS").STATEFP
pop = pd.read_csv(Z.open("co-est2025-alldata.csv"), encoding="latin-1", dtype={"STATE": str, "COUNTY": str})
pop = pop[pop.SUMLEV == 50].assign(GEOID=lambda p: p.STATE + p.COUNTY)
print(f"FAA registry, 16 October 2026: {len(reg):,} aircraft, {len(ref):,} make-and-model codes")
FAA registry, 16 October 2026: 317,604 aircraft, 94,115 make-and-model codes
Example 1: seats
seats = pd.to_numeric(m["NO-SEATS"], errors="coerce")
print(pd.cut(seats, [0, 1, 2, 4, 6, 10, 20, 1000], labels=["1", "2", "3-4", "5-6", "7-10", "11-20", "21+"]).value_counts(sort=False).to_string())
NO-SEATS
1 17340
2 85906
3-4 104618
5-6 36864
7-10 21656
11-20 13292
21+ 13498
Most registered aircraft have three or four seats (104,618) or two (85,906). 13,498 have 21 or more, the airliners.
Example 2: weight class by kind of aircraft
m["kind"] = m["TYPE AIRCRAFT"].map(TYPE_AIRCRAFT)
print(pd.crosstab(m.kind, m["AC-WEIGHT"]).loc[["fixed wing single engine", "fixed wing multi engine", "rotorcraft"]].to_string())
AC-WEIGHT CLASS 1 CLASS 2 CLASS 3 CLASS 4
kind
fixed wing single engine 216427 569 654 228
fixed wing multi engine 22446 8428 17798 10
rotorcraft 22887 1298 518 2834
Weight class 1, the lightest, covers 216,427 of the single-engine aeroplanes; multi-engine aeroplanes spread across classes 1 to 3, and 2,834 rotorcraft fall in class 4, which the FAA's documentation defines as unmanned aircraft up to 55 pounds: drones again.
Internal links
- Read the FAA registry β the
faa()reader both files go through - Age of the fleet β when these models were built
- Helicopters, gliders and balloons by state β the type codes on a map
- Mode S codes β the other key that joins this file to something else