Counting EVs by VIN Goes Wrong: Ten Characters Are Not One Vehicle

On this page

Problem statement

A VIN identifies one vehicle, so VIN (1-10) looks like the natural key for Washington's EV registrations. Use it with drop_duplicates or nunique and 300,142 vehicles become 18,526. The column holds only the first ten of the VIN's seventeen characters. Those encode the maker, the model and body, a check digit and the model year, the same for every car of a model built in a year. This page shows what the ten characters mean, how many vehicles share each, what deduplicating by them does to a ranking, and which column to use instead.

Quick answer

Use DOL Vehicle ID as the key: it is unique on every row. Treat VIN (1-10) as a model code, useful for grouping vehicles of the same model and year, never for counting or deduplicating vehicles.

print(f"rows: {len(ev):,}; distinct VIN (1-10): {ev.vin10.nunique():,}; distinct DOL vehicle IDs: {ev.dol_id.nunique():,}")
print(f"rows sharing their VIN prefix with another row: {ev.vin10.duplicated(keep=False).mean():.1%}")
rows: 300,142; distinct VIN (1-10): 18,526; distinct DOL vehicle IDs: 300,142
rows sharing their VIN prefix with another row: 98.8%

There are 300,142 rows, 300,142 distinct DOL vehicle IDs and only 18,526 distinct ten-character VIN prefixes. 98.8% of rows share their prefix with at least one other row.

Histogram on log-log axes of how many vehicles share each ten-character VIN prefix, from single vehicles to over a thousand. Histogram on log-log axes of how many vehicles share each ten-character VIN prefix, from single vehicles to over a thousand.
300,142 vehicles share 18,526 ten-character prefixes.

What the prefix encodes, and the right key

1. What the ten characters encode

v = ev.vin10.iloc[0]
print(f"{v}: WMI {v[:3]} (maker), VDS {v[3:8]} (model, body, drive), check digit {v[8]}, model-year code {v[9]}")
yr = dict(zip("ABCDEFGHJKLMNPRSTVWXY123456789", range(2010, 2040)))
print(f"10th character matches the model year: {(ev.vin10.str[9].map(yr) == ev.model_year).mean():.1%}")
3FMTK3SU8P: WMI 3FM (maker), VDS TK3SU (model, body, drive), check digit 8, model-year code P
10th character matches the model year: 100.0%

A VIN's first three characters are the world manufacturer identifier (3FM, Ford Mexico), the next five describe the vehicle (model, body, drivetrain, restraints), the ninth is a check digit, and the tenth is the model year: P is 2023. The tenth character matches the Model Year column in 100.0% of rows. The serial number that makes a VIN unique is in characters 12–17, which the state does not publish.

2. How many vehicles share a prefix

g = ev.groupby("vin10").size().sort_values(ascending=False)
print(g.head(5).to_dict())
top = ev[ev.vin10 == g.index[0]]
print(f"{g.index[0]}: {len(top):,} vehicles, {top.make.iloc[0]} {top.model.iloc[0]} {top.model_year.iloc[0]}, in {top.county.nunique()} counties")
{'7SAYGDEE9T': 1229, '7SAYGDEE7P': 1228, '7SAYGDEE4T': 1216, '7SAYGDEE1T': 1212, '7SAYGDEE6P': 1210}
7SAYGDEE9T: 1,229 vehicles, TESLA MODEL Y 2026, in 34 counties

The most common prefix, 7SAYGDEE9T, is shared by 1,229 vehicles: 2026 Tesla Model Ys registered in 34 counties. The next four are also Model Y prefixes, differing in the check digit or year. Each prefix is one configuration of one model, not one car.

Horizontal bar chart of the twelve most shared VIN prefixes, each labelled Tesla Model Y 2023 or 2026, with about twelve hundred vehicles each. Horizontal bar chart of the twelve most shared VIN prefixes, each labelled Tesla Model Y 2023 or 2026, with about twelve hundred vehicles each.
The most shared prefixes are each one Tesla Model Y configuration and year.

3. Use the DOL ID

print(f"drop_duplicates on VIN prefix keeps {ev.drop_duplicates('vin10').shape[0]:,} rows; on DOL ID {ev.drop_duplicates('dol_id').shape[0]:,}")
drop_duplicates on VIN prefix keeps 18,526 rows; on DOL ID 300,142

Deduplicating on the prefix keeps 18,526 rows and silently discards more than nine vehicles in ten. Deduplicating on DOL Vehicle ID keeps all 300,142, because the file has no repeated vehicles to remove.

Rankings and matching

The data used on this page

The Washington State Department of Licensing's Electric Vehicle Population Data, saved on this site on 15 October 2026.

from pathlib import Path

import geopandas as gpd
import numpy as np
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
# Washington State Department of Licensing, via data.wa.gov: Electric Vehicle Population Data (f6w7-q2d2), one row per
# registered battery-electric or plug-in hybrid vehicle, and Electric Vehicle Population Size History By County (3d5d-sdqb),
# monthly counts since 2017. Both are replaced monthly, so they are read from copies saved on this site on 15 October 2026.
FILES = ["wa-ev-population-2026-10-15.zip", "wa-ev-population-history-2026-10-15.zip"]
for name in FILES:
    if not Path(f"data/{name}").exists():
        Path(f"data/{name}").write_bytes(requests.get(f"https://data.spatialworkflow.io/{name}", timeout=600).content)
NAMES = {"VIN (1-10)": "vin10", "County": "county", "City": "city", "State": "state", "Postal Code": "zip", "Model Year": "model_year",
         "Make": "make", "Model": "model", "Electric Vehicle Type": "ev_type", "Clean Alternative Fuel Vehicle (CAFV) Eligibility": "cafv",
         "Electric Range": "range_mi", "Legislative District": "district", "DOL Vehicle ID": "dol_id", "Vehicle Location": "location",
         "Electric Utility": "utility", "2020 GEOID": "tract"}
ev = pd.read_csv("data/wa-ev-population-2026-10-15.zip", dtype={"Postal Code": str, "2020 GEOID": str}).rename(columns=NAMES)
ev["kind"] = ev.ev_type.str.extract(r"\((BEV|PHEV)\)")[0]
hist = pd.read_csv("data/wa-ev-population-history-2026-10-15.zip")
hist["date"] = pd.to_datetime(hist.Date, format="%B %d %Y")
print(f"{len(ev):,} electric vehicles registered with Washington DOL; history: {hist.date.min():%b %Y} to {hist.date.max():%b %Y}")
300,142 electric vehicles registered with Washington DOL; history: Jan 2017 to Sep 2026

Example 1: what deduplicating by VIN does to a ranking

wrong = ev.drop_duplicates("vin10").make.value_counts().head(5)
right = ev.make.value_counts().head(5)
print("by VIN prefix:", wrong.to_dict())
print("by vehicle:", right.to_dict())
by VIN prefix: {'TESLA': 1790, 'VOLVO': 1695, 'FORD': 1427, 'AUDI': 1359, 'CHEVROLET': 1331}
by vehicle: {'TESLA': 122291, 'CHEVROLET': 20218, 'FORD': 15997, 'NISSAN': 15994, 'KIA': 14897}

Counting distinct prefixes by make ranks makers by how many model configurations they sell, not how many cars: Tesla 1,790, Volvo 1,695, Ford, Audi and Chevrolet. By vehicles, Tesla has 122,291, six times Chevrolet, and Volvo and Audi drop out of the top five. A ranking from distinct VINs measures the variety of a maker's line-up.

Two bar charts of the top five makes: counted by vehicle, Tesla far ahead of Chevrolet, Ford, Nissan and Kia; counted by distinct VIN prefix, Tesla, Volvo, Ford, Audi and Chevrolet close together. Two bar charts of the top five makes: counted by vehicle, Tesla far ahead of Chevrolet, Ford, Nissan and Kia; counted by distinct VIN prefix, Tesla, Volvo, Ford, Audi and Chevrolet close together.
Top five makes counted by vehicle and by distinct VIN prefix.

Example 2: matching to another file

print(f"prefixes with more than one model year: {(ev.groupby('vin10').model_year.nunique() > 1).sum()}; with more than one model name: "
      f"{(ev.groupby('vin10').model.nunique() > 1).sum()}")
prefixes with more than one model year: 0; with more than one model name: 0

No prefix spans more than one model year or model name, so the prefix is a clean model-and-year code. That makes it useful for joining to a table of VIN-decoded specifications, such as the NHTSA vPIC decoder, to fill in battery size or range, and useless for matching individual vehicles across files.

Why only ten characters

A full VIN identifies a single vehicle, and with registration records it could identify its owner. The Department of Licensing publishes the first ten characters, enough to decode make, model, body and year, and drops the plant code and serial number. It adds its own DOL Vehicle ID, a key without meaning outside the department, so that rows can be counted and tracked between monthly releases without exposing the vehicle. The same truncation appears in other public vehicle datasets, so check any VIN column's length before using it as a key.

Key checks

  • Test uniqueness before using any column as a key: df[col].is_unique.
  • The DOL ID is the department's own vehicle key; use it to compare releases rather than any VIN fragment.
  • Decoding the prefix with NHTSA's vPIC API works on ten characters for most makers; send one request per distinct prefix, not per vehicle.
  • The check digit varies within a model, so two prefixes can be the same model and year.

FAQ

Is VIN (1-10) unique in the Washington EV data?

No. 300,142 vehicles share 18,526 ten-character prefixes; the most common is shared by 1,229 Tesla Model Ys. Use DOL Vehicle ID.

What do the first ten characters of a VIN mean?

The maker (1–3), vehicle attributes such as model and body (4–8), a check digit (9) and the model year (10). The unique serial number is in characters 12–17.