html`<p class="crosslinks">${p.label} elsewhere: <a href="staffing-boroughs.html?pb=${pbCode[p.patrol_borough]}">${p.patrol_borough} patrol borough</a>${unitsPublished ?html`<span> · </span><a href="staffing-units.html?unit=${encodeURIComponent(unitName(p.pct))}">unit view, with ranks</a>${p.now.ftu>0?html`<span> · </span><a href="staffing-units.html?unit=${encodeURIComponent(p.ftu_shared_midtown?"TIMES SQUARE FIELD TRAINING UNIT":String(p.pct).padStart(3,"0") +" PCT FIELD TRAINING UNIT")}">its FTU</a>`:""}<span> · </span><a href="staffing-units.html?unit=${encodeURIComponent(({14:"MTS DET SQUAD",18:"MTN DET SQUAD"})[p.pct] ??String(p.pct).padStart(3,"0") +" DET SQUAD")}">its detective squad</a>`:""}</p>`
html`<div class="kpis four"> <div class="kpi"><div class="v">${fmtN(last.total)}</div><div class="l">officers, ${fmtM(last.date)}<br><span class="meta"><strong>${change(last.total, first.total)}</strong> since ${fmtM(first.date)} · all precincts ${change(cwLast.total, cwFirst.total)}</span></div></div> <div class="kpi"><div class="v">${fmtN(dsLast.n)}</div><div class="l">detective squad<br><span class="meta"><strong>${change(dsLast.n, dsFirst.n)}</strong> since ${fmtM(dsFirst.date)}</span></div></div> <div class="kpi"><div class="v">${fmtR(p.yos.median)}<span class="u">yrs</span></div><div class="l">median time on the job<br><span class="meta">all precincts ${fmtR(cw.yos.median)} · department ${fmtR(cw.dept_yos.median)}</span></div></div> <div class="kpi"><div class="v">1 : ${fmtR(p.sgt.cops_per_sgt)}</div><div class="l">sergeants to officers<br><span class="meta">all precincts 1 : ${fmtR(cw.sgt.cops_per_sgt)}</span></div></div></div>`
Compare year over year. Month-to-month movement is mostly roster timing, not a change in strength: academy classes arrive in steps, summer details pull officers out of the command, and retirements cluster. The same-month comparisons below are the fair ones.
Officers assigned to the precinct each month. The field training unit and the DV squad are stacked inside the total; the thin line is the total itself.
html`<p class="note">${p.now.ftu>0?html`<strong>Field training unit:</strong> ${fmtN(p.now.ftu)} rookies in the ${p.ftu_shared_midtown?"Times Square FTU (shared by Midtown South and North, split 52/48)": p.label+" FTU"}, counted in the total. `:html`<strong>Field training unit:</strong> none. `}${p.now.dv>0?html`<strong>DV squad:</strong> ${fmtN(p.now.dv)} officers, carved out of the precinct roster in October 2025, counted in the total. `:html`<strong>DV squad:</strong> none listed. `}${detailsNote.length?html`<strong>Not counted:</strong> ${detailsNote.map(([k, v]) =>`${fmtN(v)} in ${k.replace(/^\d{3} PCT /,"").toLowerCase().replace(/\b\w/g, c => c.toUpperCase())}`).join("; ")} (a detail carried under the precinct's name, staffed largely from elsewhere).`:""}</p>`
Detective squad
html`<table class="same-month"> <caption>Same month, each year · separate command, not in the precinct total</caption> <thead><tr><th></th>${dsSameMonth.map(d =>html`<th>${fmtM(d.date)}</th>`)}</tr></thead> <tbody> <tr><th>Investigators</th>${dsSameMonth.map(d =>html`<td>${fmtN(d.n)}</td>`)}</tr> <tr><th>vs. prior year</th>${dsSameMonth.map((d, i) =>html`<td class="meta">${i ?change(d.n, dsSameMonth[i -1].n) :"—"}</td>`)}</tr> </tbody></table>`
Plot.plot({ width,height:180,marginLeft:44,style: {fontFamily:"inherit",fontSize:"13px",background:"transparent",color:"#29251f"},x: {label:null},y: {label:"Detective squad",grid:true,domain: [0, d3.max(ds, d => d.n) *1.15]},marks: [ Plot.areaY(ds, {x:"date",y:"n",fill:"#7a2e2e",fillOpacity:0.12}), Plot.lineY(ds, {x:"date",y:"n",stroke:"#7a2e2e",strokeWidth:1.5}), Plot.ruleY([0]), Plot.tip(ds, Plot.pointerX({x:"date",y:"n",title: d =>`${fmtM(d.date)}\n${fmtN(d.n)} in squad`})) ]})
Investigators assigned to the precinct detective squad — a separate command, not counted in the precinct total.
Roster on . Sergeants include special-assignment and detective-squad sergeants; lieutenants include detective-squad commanders; detectives of all grades and specialists are grouped.
Time on the job
yosBins = d3.range(data.yos_bins+1)yosP = yosBins.map(i => ({yrs: i,share: p.yos.hist[i] / p.yos.n}))yosC = yosBins.map(i => ({yrs: i,share: cw.yos.hist[i] / cw.yos.n}))Plot.plot({ width,height:260,marginLeft:44,style: {fontFamily:"inherit",fontSize:"13px",background:"transparent",color:"#29251f"},x: {label:"Years since appointment",domain: [0, data.yos_bins+1],ticks: d3.range(0, data.yos_bins+1,5),tickFormat: d => d === data.yos_bins?"30+": d},y: {label:"Share of officers",grid:true,tickFormat:".0%"},marks: [ Plot.rectY(yosP, {x1:"yrs",x2: d => d.yrs+1,y:"share",fill:"#7a2e2e",fillOpacity:0.75,insetLeft:0.5,insetRight:0.5}), Plot.lineY(yosC, {x:"yrs",y:"share",stroke:"#29251f",strokeWidth:1.5,curve:"step-after"}), Plot.ruleX([p.yos.median], {stroke:"#7a2e2e",strokeDasharray:"3,3"}), Plot.ruleX([cw.yos.median], {stroke:"#29251f",strokeDasharray:"3,3"}), Plot.tip(yosP, Plot.pointerX({x: d => d.yrs+0.5,y:"share",title: d =>`${d.yrs=== data.yos_bins?"30+": d.yrs+"–"+ (d.yrs+1)} years\n${p.label}: ${d3.format(".1%")(d.share)}\nAll precincts: ${d3.format(".1%")(yosC[d.yrs].share)}`})), Plot.ruleY([0]) ]})
Bars: (median yrs). Step line: all precincts pooled (median yrs). Both include FTU rookies and DV squads. Whole department, including headquarters and specialized commands: yrs.
Supervision
ratioRows = [ {who: p.label,ratio: p.sgt.cops_per_sgt,sgts: p.sgt.sgts,cops: p.sgt.po_det}, {who:"All precincts",ratio: cw.sgt.cops_per_sgt,sgts: cw.sgt.sgts,cops: cw.sgt.po_det}]Plot.plot({ width,height:90,marginLeft:110,style: {fontFamily:"inherit",fontSize:"13px",background:"transparent",color:"#29251f"},x: {label:"Police officers and detectives per sergeant",grid:true,domain: [0,Math.max(16, d3.max(ratioRows, d => d.ratio) *1.15)]},y: {label:null,domain: ratioRows.map(d => d.who)},marks: [ Plot.barX(ratioRows, {x:"ratio",y:"who",fill: d => d.who=== p.label?"#7a2e2e":"#a89a7b"}), Plot.text(ratioRows, {x:"ratio",y:"who",text: d =>fmtR(d.ratio),dx:6,textAnchor:"start"}), Plot.ruleX([0]) ]})
: sergeants for police officers and detectives. All precincts: for . Detective squad: (all squads 1 : ). Precincts with an FTU run higher because rookies count against the precinct’s sergeants.
Definitions.Assigned means listed under the command in the public records and confirmed as still active, so retirees who linger on a roster are not counted. A precinct’s total is the precinct proper plus its Field Training Unit (its own rookies, precinct-named since August 2025) and its Domestic Violence Squad (carved out of precinct rosters in October 2025). The Times Square FTU serves both Midtown precincts and is split 52/48. Seasonal and standing details carried under a precinct’s name are reported but not counted. Detective squads are separate commands. Years on the job run from appointment date; the sergeant ratio divides police officers and detectives by sergeants of all designations.
Continuity. The series is continuous from January 2023. Each month’s count is taken at month end; where sources overlap they agree within about one percent. The latest month is as of .
Hall, J. (2026). NYPD Precinct Staffing Dashboard. https://johnjhallresearch.com/staffing. Accessed [date].