Warren Sharp gives sports bettors predictive NFL analytics & visualized data, winning 60% on game totals and beating NFL lines with the sharpest edge in betting
What personnel packages do NFL offenses use the most?
The table below tracks how often all 32 teams line up in 11, 12, 13, 21, and 22 personnel, and what each grouping is actually worth per play.
Filter by season, week, down, field zone, score margin, or play type to isolate the situations you care about, then switch to efficiency to see EPA per play, yards per play, and success rate with league ranks.
NFL Offensive Personnel Usage by Team
Personnel Grouping Frequency
Loading play-by-play data…
2025 personnel grouping frequency by offense, all offensive plays, no filters applied. Groupings use the standard code: the first digit is running backs, the second is tight ends (so "12" is 1 RB and 2 TE).
#
Offense
11
12
13
21
22
2+ TE
2+ RB
3+ WR
Plays
1
ARI
51%
36%
11%
0%
0%
47%
0%
49%
1,059
2
ATL
46%
38%
7%
9%
0%
45%
10%
46%
1,032
3
BAL
30%
35%
6%
18%
9%
51%
28%
31%
954
4
BUF
62%
12%
5%
9%
12%
29%
20%
63%
1,056
5
CAR
67%
21%
8%
2%
1%
30%
3%
68%
987
6
CHI
53%
36%
8%
0%
0%
45%
0%
55%
1,086
7
CIN
66%
30%
1%
2%
0%
31%
2%
67%
1,041
8
CLE
46%
45%
4%
2%
2%
51%
4%
46%
1,013
9
DAL
68%
17%
2%
8%
3%
23%
12%
69%
1,100
10
DEN
65%
14%
2%
8%
8%
24%
16%
67%
1,072
11
DET
62%
25%
5%
3%
0%
30%
6%
63%
1,049
12
GB
61%
33%
1%
3%
0%
35%
3%
57%
990
13
HOU
72%
12%
1%
7%
1%
14%
11%
71%
1,072
14
IND
64%
26%
9%
1%
0%
36%
1%
64%
1,002
15
JAX
71%
22%
1%
4%
0%
24%
4%
68%
1,070
16
KC
58%
31%
7%
4%
0%
38%
5%
57%
1,042
17
LAC
58%
6%
2%
15%
7%
15%
29%
69%
1,075
18
LAR
60%
9%
31%
0%
0%
40%
0%
60%
1,056
19
LV
58%
35%
6%
0%
0%
42%
1%
58%
936
20
MIA
42%
12%
1%
30%
8%
21%
44%
42%
939
21
MIN
65%
20%
3%
8%
4%
27%
12%
65%
939
22
NE
52%
21%
0%
19%
6%
28%
26%
51%
1,013
23
NO
69%
19%
7%
0%
0%
30%
1%
70%
1,030
24
NYG
61%
33%
5%
1%
0%
38%
1%
61%
1,073
25
NYJ
68%
20%
0%
9%
2%
23%
11%
68%
997
26
PHI
63%
29%
6%
1%
0%
36%
1%
61%
988
27
PIT
49%
33%
13%
2%
1%
48%
4%
39%
971
28
SEA
42%
28%
3%
14%
9%
43%
24%
45%
994
29
SF
43%
11%
0%
37%
7%
20%
45%
43%
1,060
30
TB
70%
21%
3%
5%
0%
24%
5%
71%
1,056
31
TEN
73%
23%
1%
3%
0%
24%
3%
71%
982
32
WAS
59%
30%
5%
2%
1%
37%
3%
60%
969
Hover or tap any cell for EPA per play, yards per play and success rate with league
ranks. Efficiency needs 20+ plays to show. Kneels, spikes and plays with incomplete
personnel data are excluded.
';
}).join('') + '
'+SIX_OL+'
';
tog.addEventListener('click', function(){
var open = box.hasAttribute('hidden');
if (open) box.removeAttribute('hidden'); else box.setAttribute('hidden','');
tog.setAttribute('aria-expanded', open ? 'true' : 'false');
tog.textContent = open ? 'Hide grouping descriptions' : 'What do the groupings mean?';
});
}
function wireModes(){
Array.prototype.forEach.call($('pg-modes').children, function(b){
b.addEventListener('click', function(){
S.mode = b.dataset.m;
Array.prototype.forEach.call($('pg-modes').children, function(o){
o.setAttribute('aria-pressed', o === b ? 'true' : 'false');
});
hideTip(); render();
});
});
}
function clearAll(){
['weeks','qtrs','downs','zones','rbs','tes','wrs'].forEach(function(k){ S[k] = new Set(); });
S.play = 'all'; S.dist = [null,null]; S.margin = [null,null];
S.neutral = false; S.open = null;
var keep = Array.from(S.seasons);
S.seasons = new Set(keep.length ? [Math.max.apply(null, keep)] : []);
closePop(); hideTip(); syncBar(); render();
}
function activeCount(){
var n = 0;
DEFS.forEach(function(d){ if (!isDefault(d)) n++; });
return n;
}
/* ---------- aggregate ---------- */
function keep(D, i, ignoreWeek){
if (!ignoreWeek && S.weeks.size && !S.weeks.has(D.w[i])) return false;
if (S.qtrs.size && !S.qtrs.has(D.q[i])) return false;
if (S.downs.size && !S.downs.has(D.d[i])) return false;
if (S.zones.size && !S.zones.has(D.z[i])) return false;
if (S.play === 'drop' && D.b[i] !== 1) return false;
if (S.play === 'run' && D.b[i] !== 0) return false;
if (S.dist[0] !== null && (D.dist[i] < S.dist[0] || D.dist[i] > S.dist[1])) return false;
if (S.margin[0] !== null && (D.m[i] < S.margin[0] || D.m[i] > S.margin[1])) return false;
if (S.rbs.size && !(S.rbs.has(D.rb[i]) || (S.rbs.has(3) && D.rb[i] >= 3))) return false;
if (S.tes.size && !(S.tes.has(D.te[i]) || (S.tes.has(3) && D.te[i] >= 3))) return false;
if (S.wrs.size && !(S.wrs.has(D.wr[i]) || (S.wrs.has(4) && D.wr[i] >= 4))) return false;
return true;
}
function bucket(){ return {n:0, epa:0, yds:0, sc:0}; }
function add(b, D, i){ b.n++; b.epa += D.epa[i]; b.yds += D.yds[i]; b.sc += D.sc[i]; }
function blank(){
var o = {plays:0, heavy:0, spread:0, other:0, g:{}, exact:{}, cell:{}, all:bucket()};
GROUPS.forEach(function(c){ o.cell['g'+c] = bucket(); });
SUMS.forEach(function(s){ o.cell[s.k] = bucket(); });
return o;
}
function tally(row, D, i){
row.plays++;
add(row.all, D, i);
var rb = D.rb[i], te = D.te[i], wr = D.wr[i], g = D.grp[i];
row.g[g] = (row.g[g]||0)+1;
if (row.cell['g'+g]) add(row.cell['g'+g], D, i);
if (te >= 2) add(row.cell.te2, D, i);
if (rb >= 2) add(row.cell.rb2, D, i);
if (wr >= 3) add(row.cell.wr3, D, i);
if (rb >= 2 || te >= 2) row.heavy++;
else if (wr >= 3) row.spread++;
else row.other++;
var ex = rb+'-'+te+'-'+wr;
row.exact[ex] = (row.exact[ex]||0)+1;
}
function aggregate(){
var teams = {}, league = blank();
Array.from(S.seasons).forEach(function(y){
var D = DB[y];
if (!D) return;
for (var i=0;i
'+
'
'+INFO[key]+'
', false);
}
function showTip(cell, team, key, label, tap){
var A = LAST;
if (!A) return;
var b = A.teams[team].cell[key];
var html = '
'+team+' \u00b7 '+label+'
';
if (b.n < MIN_EPA_PLAYS){
html += '
Only '+b.n+' play'+(b.n===1?'':'s')+
' match these filters \u2014 below the '+MIN_EPA_PLAYS+
'-play minimum, so efficiency is not shown.
';
} else {
var R = A.ranks[key], rows = [
['EPA/play', sgn(b.epa/b.n), R.epa[team]],
['Yards/play', (b.yds/b.n).toFixed(1), R.yds[team]],
['Success', (100*b.sc/b.n).toFixed(1)+'%', R.sc[team]]
];
html += '
' + rows.map(function(r){
return ''+r[0]+''+r[1]+''+ord(r[2])+' of '+R.size+'';
}).join('') + '
';
if (R.size < 32) html += '
Ranked among the '+R.size+
' offenses with '+MIN_EPA_PLAYS+'+ plays here.
';
}
placeTip(cell, html, tap);
}
function wireCell(td, team, key, label){
td.classList.add('pt-hascell');
td.tabIndex = 0;
/* Hover is mouse-only. On touch, a tap synthesises mouseenter immediately
before click, which would open then instantly close the panel — so gate
on pointerType and let the click handler own the tap. */
if (window.PointerEvent){
td.addEventListener('pointerenter', function(e){
if (e.pointerType === 'mouse') showTip(td, team, key, label, false);
});
td.addEventListener('pointerleave', function(e){
if (e.pointerType === 'mouse') hideTip();
});
} else {
td.addEventListener('mouseenter', function(){ showTip(td, team, key, label, false); });
td.addEventListener('mouseleave', hideTip);
}
td.addEventListener('focus', function(){ showTip(td, team, key, label, false); });
td.addEventListener('blur', hideTip);
td.addEventListener('click', function(e){
e.stopPropagation();
if (TIPPED === td) hideTip();
else showTip(td, team, key, label, true);
});
}
/* ---------- charts ---------- */
/* All three charts share a 300x180 viewBox so they render at identical size
next to each other, and the CSS caps them at 340px so they cannot balloon
on a very wide screen. */
var CW = 300, CH = 180;
function niceStep(max){
var steps = [2, 5, 10, 20, 25, 50];
for (var i = 0; i < steps.length; i++) if (max / steps[i] <= 4) return steps[i];
return 100;
}
function donut(row){
var slices = [
{k:'heavy', n:row.heavy, c:C_RED, l:'Heavy (2+ RB/TE)'},
{k:'spread', n:row.spread, c:C_G1, l:'3+ WR'},
{k:'other', n:row.other, c:C_G2, l:'Other'}
];
var R = 52, CX = 76, CY = 88, SW = 21, CIRC = 2*Math.PI*R, off = 0, g = '';
slices.forEach(function(sl){
if (!sl.n) return;
var len = CIRC * sl.n / row.plays;
g += '';
off += len;
});
var big = slices.slice().sort(function(a,b){ return b.n-a.n; })[0];
g += ''+
'';
slices.forEach(function(sl, i){
var y = 52 + i*38;
g += ''+
''+
'';
});
return '
' + svg(CW, CH, g) +
'
Three mutually exclusive buckets, so these total 100% \u2014 '+
'unlike the table columns, which overlap.
';
}
function trend(pts, team){
var live = pts.filter(function(p){ return p.row && p.row.plays >= 5; });
if (live.length < 2){
return '
Not enough weeks match these '+
'filters to draw a trend.
';
}
var L = 26, R = 8, T = 10, B = 26, iw = CW-L-R, ih = CH-T-B;
var x = function(i){ return pts.length < 2 ? L : L + iw*i/(pts.length-1); };
var y = function(v){ return T + ih*(1 - v/100); };
var g = '';
[0,25,50,75,100].forEach(function(v){
g += ''+
'';
});
var series = [{k:'te2',c:C_INK,l:'2+ TE'},{k:'rb2',c:C_RED,l:'2+ RB'},{k:'wr3',c:C_G1,l:'3+ WR'}];
series.forEach(function(sr){
var run = [], segs = [];
pts.forEach(function(p, i){
if (p.row && p.row.plays >= 5) run.push([x(i), y(pct(p.row.cell[sr.k].n, p.row.plays))]);
else { if (run.length > 1) segs.push(run); run = []; }
});
if (run.length > 1) segs.push(run);
segs.forEach(function(seg){
g += '';
seg.forEach(function(q){
g += '';
});
});
});
pts.forEach(function(p, i){
if (pts.length <= 10 || i === 0 || i === pts.length-1 || (i+1) % 3 === 0)
g += '';
});
var order = [];
pts.forEach(function(p){ if (order.indexOf(p.season) < 0) order.push(p.season); });
if (order.length > 1) order.forEach(function(yr){
var idx = [];
pts.forEach(function(p, i){ if (p.season === yr) idx.push(i); });
g += '';
});
var key = series.map(function(sr){
return ''+sr.l+'';
}).join('');
return '
' + svg(CW, CH, g) + '
'+key+'
' +
'
Every filter except week. Weeks under 5 plays are skipped.'+
'
';
}
/* Horizontal bars: groupings down the Y axis, share of plays along X, and the
gap to league average called out above each bar. */
/* Vertical bars: groupings across the X axis, share of plays up the Y, with the
gap to league average called out above each bar. */
function groupBars(row, league){
var ranked = Object.keys(row.g).map(Number)
.sort(function(a,b){ return row.g[b]-row.g[a]; });
/* A grouping with three snaps is a rounding error, not a tendency — it would
occupy a slot reading "0%". Keep it out unless there is nothing else. */
var codes = ranked.filter(function(c){ return row.g[c] >= 5; }).slice(0, 6);
if (codes.length < 3) codes = ranked.slice(0, 3);
if (!codes.length) return '
No plays match these filters.
';
var L = 28, R = 8, T = 26, B = 26;
var plotW = CW - L - R, plotH = CH - T - B;
var top = Math.max.apply(null, codes.map(function(c){
return Math.max(pct(row.g[c], row.plays), pct(league.g[c]||0, league.plays));
}));
var step = niceStep(top), axisMax = Math.max(step, Math.ceil(top/step)*step);
var y = function(v){ return T + plotH * (1 - v/axisMax); };
var slot = plotW / codes.length, barW = Math.min(38, slot * 0.56);
var g = '';
for (var v = 0; v <= axisMax + 0.001; v += step){
g += ''+
'';
}
codes.forEach(function(c, i){
var p = pct(row.g[c], row.plays), lp = pct(league.g[c]||0, league.plays);
var diff = p - lp;
var cx = L + slot*(i + 0.5), x0 = cx - barW/2;
var barTop = y(p), base = y(0);
g += '';
/* dashed link when the league mark sits clear of the bar top */
if (Math.abs(y(lp) - barTop) > 8){
g += '';
}
g += '';
var lab = Math.min(barTop, y(lp));
g += '';
g += '';
g += '';
});
return '
' + svg(CW, CH, g) +
'
Share of plays up the side. Bar is this offense, the black '+
'tick is the league average, and the figure on top is the gap between them.
';
}
/* ---------- render ---------- */
/* Rank always means "position when this column is ordered best-first", so it
stays correct when the table is sorted ascending. Ties share a rank. */
function rankBy(names, valueOf){
var pool = names.filter(function(t){ return valueOf(t) !== null; })
.sort(function(a, b){ return valueOf(b) - valueOf(a); });
var map = {}, prevVal = null, prevRank = 0;
pool.forEach(function(t, i){
var v = valueOf(t);
if (prevVal !== null && v === prevVal) map[t] = prevRank;
else { map[t] = i + 1; prevRank = i + 1; }
prevVal = v;
});
return map;
}
function cellValue(b, teamPlays){
if (S.mode === 'epa') return b.n >= MIN_EPA_PLAYS ? b.epa/b.n : null;
return pct(b.n, teamPlays);
}
function render(){
var A = aggregate();
LAST = A;
var L = A.league;
var names = Object.keys(A.teams);
var yrs = Array.from(S.seasons).sort();
var scope = yrs.length > 1 ? yrs[0]+'\u2013'+yrs[yrs.length-1] : String(yrs[0] || '');
$('pg-sub').innerHTML = ''+fmt(L.plays)+' plays \u00b7 '+names.length+' offenses \u00b7 '+
scope + (activeCount() ? ' \u00b7 filtered' : ' \u00b7 all plays');
var body = $('pg-body');
if (!L.plays){
body.innerHTML = '';
body.appendChild(el('div','pt-msg','No plays match those filters'+
'Widen the score margin or clear a filter to bring rows back.'));
return;
}
var epaMode = S.mode === 'epa';
function metricOf(t, c){
var r = A.teams[t];
return cellValue(r.cell[c.key], r.plays);
}
var COLS = GROUPS.map(function(c){
return {key:'g'+c, label:String(c), sort:c, cls:'pt-g'};
}).concat(SUMS.map(function(s, i){
return {key:s.k, label:s.l, sort:s.k, cls:'pt-g'+(i===0?' pt-sfirst':''), summary:true};
}));
/* scale for shading */
var maxU = 0, maxE = 0;
names.forEach(function(t){
var r = A.teams[t];
COLS.forEach(function(c){
var b = r.cell[c.key];
maxU = Math.max(maxU, pct(b.n, r.plays));
if (b.n >= MIN_EPA_PLAYS) maxE = Math.max(maxE, Math.abs(b.epa/b.n));
});
});
if (!maxE) maxE = 0.1;
var sortCol = null;
COLS.forEach(function(c){ if (String(c.sort) === String(S.sort)) sortCol = c; });
var rank = (S.sort === 'team' || S.sort === 'plays' || !sortCol)
? rankBy(names, function(t){ return A.teams[t].plays; })
: rankBy(names, function(t){ return metricOf(t, sortCol); });
if (S.sort === 'team') names.sort(function(a,b){ return S.dir*a.localeCompare(b); });
else if (S.sort === 'plays') names.sort(function(a,b){
return S.dir*(A.teams[b].plays - A.teams[a].plays); });
else {
var col = sortCol || COLS[0];
names.sort(function(a,b){
var va = metricOf(a, col), vb = metricOf(b, col);
if (va === null && vb === null) return a.localeCompare(b);
if (va === null) return 1;
if (vb === null) return -1;
return S.dir*(vb - va) || a.localeCompare(b);
});
}
var tbl = el('table','pt-grid');
tbl.appendChild(el('caption', null, epaMode
? 'EPA per play by personnel grouping. Positive is good for the offense. '+
'Cells need '+MIN_EPA_PLAYS+'+ plays; thinner samples show a dash.'
: 'Each grouping as a share of the offense\u2019s plays. Hover or tap a cell for '+
'EPA per play, yards per play and success rate.'));
function sortBtn(key, label, cls){
var th = el('th', cls || null);
th.scope = 'col';
if (String(S.sort) === String(key)){
var asc = (key === 'team') ? S.dir > 0 : S.dir < 0;
th.setAttribute('aria-sort', asc ? 'ascending' : 'descending');
}
var b = el('button', null, label);
b.type = 'button';
b.addEventListener('click', function(){
if (String(S.sort) === String(key)) S.dir = -S.dir;
else { S.sort = key; S.dir = 1; }
hideTip(); render();
});
th.appendChild(b);
return th;
}
var thead = el('thead'), hr = el('tr');
hr.appendChild(el('th','pt-rank','#'));
hr.appendChild(sortBtn('team','Offense','pt-team'));
COLS.forEach(function(c){
var th = sortBtn(c.sort, c.label, c.cls);
if (INFO[c.key]){
th.classList.add('pt-hascell');
if (window.PointerEvent){
th.addEventListener('pointerenter', function(e){
if (e.pointerType === 'mouse') showInfo(th, c.label, c.key);
});
th.addEventListener('pointerleave', function(e){
if (e.pointerType === 'mouse') hideTip();
});
} else {
th.addEventListener('mouseenter', function(){ showInfo(th, c.label, c.key); });
th.addEventListener('mouseleave', hideTip);
}
}
hr.appendChild(th);
});
hr.appendChild(sortBtn('plays','Plays','pt-plays'));
thead.appendChild(hr);
tbl.appendChild(thead);
var ncols = COLS.length + 3;
var tb = el('tbody');
names.forEach(function(t){
var r = A.teams[t], tr = el('tr','pt-row');
if (S.open === t) tr.classList.add('open');
tr.appendChild(el('td','pt-rank', rank[t] ? String(rank[t]) : '\u2013'));
var th = el('th','pt-team');
th.scope = 'row';
var btn = el('button', null, t);
btn.type = 'button';
btn.setAttribute('aria-expanded', S.open === t ? 'true' : 'false');
btn.addEventListener('click', function(e){
e.stopPropagation();
hideTip();
S.open = (S.open === t) ? null : t;
render();
});
th.appendChild(btn);
tr.appendChild(th);
COLS.forEach(function(c){
var b = r.cell[c.key];
var td = el('td', c.cls);
if (!b.n){
td.className = c.cls + ' pt-nil';
td.innerHTML = '\u2013';
tr.appendChild(td);
return;
}
if (epaMode){
var e = b.n >= MIN_EPA_PLAYS ? b.epa/b.n : null;
if (e === null){
td.className = c.cls + ' pt-nil';
td.innerHTML = '\u2013';
} else {
td.style.background = heatEpa(e/maxE);
if (Math.abs(e)/maxE > 0.62) td.style.color = '#fff';
td.innerHTML = ''+sgn(e)+'';
}
} else {
var pv = pct(b.n, r.plays);
td.style.background = heat(pv/(maxU||1));
if (pv/(maxU||1) > 0.6) td.style.color = '#fff';
td.innerHTML = ''+(pv<10?pv.toFixed(1):r0(pv))+'%'+
''+fmt(b.n)+'';
}
wireCell(td, t, c.key, c.summary ? c.label : c.label+' personnel');
tr.appendChild(td);
});
tr.appendChild(el('td','pt-plays', fmt(r.plays)));
tb.appendChild(tr);
if (S.open === t) tb.appendChild(detail(t, r, L, ncols));
});
tbl.appendChild(tb);
var tf = el('tfoot'), fr = el('tr');
fr.appendChild(el('td','pt-rank',''));
var fth = el('th','pt-team','League');
fth.scope = 'row';
fr.appendChild(fth);
COLS.forEach(function(c){
var b = L.cell[c.key];
fr.appendChild(el('td', c.cls, epaMode
? (b.n ? sgn(b.epa/b.n) : '\u2013')
: r0(pct(b.n, L.plays))+'%'));
});
fr.appendChild(el('td','pt-plays', fmt(L.plays)));
tf.appendChild(fr);
tbl.appendChild(tf);
body.innerHTML = '';
body.appendChild(tbl);
}
function detail(team, r, L, span){
var tr = el('tr','pt-det'), td = el('td');
td.colSpan = span;
var wrap = el('div','pt-dwrap');
var head = el('div','pt-dhead');
head.innerHTML = ''+team+''+fmt(r.plays)+' plays match the current filters \u00b7 '+
sgn(r.all.epa/r.all.n)+' EPA/play \u00b7 '+(r.all.yds/r.all.n).toFixed(1)+' yds/play \u00b7 '+
(100*r.all.sc/r.all.n).toFixed(1)+'% success';
wrap.appendChild(head);
var c1 = el('div','pt-card');
c1.appendChild(el('div','pt-ctitle','Personnel mix'));
c1.appendChild(el('div', null, donut(r)));
wrap.appendChild(c1);
var c2 = el('div','pt-card');
c2.appendChild(el('div','pt-ctitle','Week by week'));
c2.appendChild(el('div', null, trend(weekly(team), team)));
wrap.appendChild(c2);
var c3 = el('div','pt-card');
c3.appendChild(el('div','pt-ctitle','Against league average'));
c3.appendChild(el('div', null, groupBars(r, L)));
wrap.appendChild(c3);
td.appendChild(wrap);
tr.appendChild(td);
return tr;
}
boot();
})();
Which NFL Teams Used 11 Personnel the Most in 2025?
In 2025, the Titans (73%), Texans (72%) and Jaguars (71%) built almost everything out of 11 personnel despite changing trends throughout the league.
At the other end, the Ravens used 11 personnel on 30% of snaps.
That was nearly half of the league average.
Heavy Personnel: 12 and 13 Personnel Usage by Team
The emerging trend in 2025 was a shift toward heavier personnel, led by the Rams utilizing three tight ends on 31% of their offensive snaps.
Look at the league average of plays with two or more tight ends on the field:
2021: 28%
2022: 27%
2023: 26%
2024: 30%
2025: 33%
With the way NFL teams spent money in free agency and used draft capital this offseason, it is clear that the trend toward heavier personnel will stick around in 2026.
What Is 11 Personnel?
11 personnel is a football formation that features one running back and one tight end.
What Is 12 Personnel?
12 personnel is a football formation that features one running back and two tight ends.
What Is 13 Personnel?
13 personnel is a football formation that features one running back and three tight ends.
What Is 21 Personnel?
21 personnel is a football formation that features two running backs and one tight end.
What Is 22 Personnel?
22 personnel is a football formation that features two running backs and two tight ends.