Every month the NYPD announces crime counts for the month just ended. A year later the same month is printed again in the anniversary release, and the number is usually different. This page tracks each month’s count across every public appearance since 2018.
html`<div class="kpis four"> <div class="kpi"><div class="v">${summary.n_up}<span class="u">of ${summary.n}</span></div><div class="l">months revised upward at one year</div></div> <div class="kpi"><div class="v">${fmtS(summary.mean_rev)}%</div><div class="l">mean revision, initial → one year<br><span class="meta">median ${fmtS(summary.median_rev)}%</span></div></div> <div class="kpi"><div class="v">${fmtN(summary.yearone_sum- summary.initial_sum)}</div><div class="l">${label} added to the record after first release<br><span class="meta">${fmtN(summary.initial_sum)} → ${fmtN(summary.yearone_sum)}, ${observed[0].m.slice(0,4)}–${observed[observed.length-1].m.slice(0,4)}</span></div></div> <div class="kpi"><div class="v">${fmtS(maxRow.rev1)}%</div><div class="l">largest revision: ${fmtM(maxRow.date)}<br><span class="meta">${fmtN(maxRow.initial)} → ${fmtN(maxRow.yearone)}</span></div></div></div>`
Plot.plot({ width,height:300,marginLeft:50,style: {fontFamily:"inherit",fontSize:"13px",background:"transparent",color:"#29251f"},x: {label:null},y: {label:`${ct}, per month`,grid:true,tickFormat:",.0f"},color: {domain: ["First announced","One year later"],range: ["#a89a7b","#7a2e2e"],legend:true},marks: [ Plot.areaY(observed, {x:"date",y1:"initial",y2:"yearone",fill:"#7a2e2e",fillOpacity:0.15}), Plot.lineY(series, {x:"date",y:"initial",stroke:"#a89a7b",strokeWidth:1.5}), Plot.lineY(observed, {x:"date",y:"yearone",stroke:"#7a2e2e",strokeWidth:1.5}), Plot.tip(series, Plot.pointerX({x:"date",y: d => d.yearone?? d.initial,title: d =>`${fmtM(d.date)}\nFirst announced: ${fmtN(d.initial)} (${fmtD(parseD(d.v0))})`+ (d.yearone!=null?`\nOne year later: ${fmtN(d.yearone)} (${fmtD(parseD(d.v1))}) · ${fmtS(d.rev1)}%`:"\nOne year later: not yet published") + (d.latest!== d.yearone&& d.latest!== d.initial?`\nLatest: ${fmtN(d.latest)} (${fmtD(parseD(d.vl))})`:"")})) ]})
Each reference month as first announced and as re-reported a year later. The shaded band is the difference between the two.
Plot.plot({ width,height:200,marginLeft:50,style: {fontFamily:"inherit",fontSize:"13px",background:"transparent",color:"#29251f"},x: {label:null},y: {label:"Revision at one year (%)",grid:true,tickFormat: d => d3.format("+d")(d)},marks: [ Plot.rectY(observed, {x1: d => d.date,x2: d => d3.utcMonth.offset(d.date,1),y:"rev1",fill: d => d.rev1>=0?"#7a2e2e":"#27506b",insetLeft:0.5,insetRight:0.5}), Plot.ruleY([0]), Plot.tip(observed, Plot.pointerX({x:"date",y:"rev1",title: d =>`${fmtM(d.date)}: ${fmtS(d.rev1)}%\n${fmtN(d.initial)} → ${fmtN(d.yearone)}`})) ]})
Percent change between the first announcement and the anniversary release, by reference month.
Plot.plot({ width,height:34+22* byYear.length,marginLeft:50,marginBottom:30,style: {fontFamily:"inherit",fontSize:"13px",background:"transparent",color:"#29251f"},padding:0.08,x: {label:null,domain: d3.range(1,13),tickFormat: m => d3.utcFormat("%b")(newDate(Date.UTC(2000, m -1,1)))},y: {label:null,domain: byYear.map(d =>String(d.year)).reverse()},color: {type:"diverging",pivot:0,scheme:"RdBu",reverse:true,domain: [-d3.max(observed, d =>Math.abs(d.rev1)), d3.max(observed, d =>Math.abs(d.rev1))],legend:true,label:"Revision at one year (%)",tickFormat: d => d3.format("+d")(d)},marks: [ Plot.cell(observed, {x: d => d.date.getUTCMonth() +1,y: d =>String(d.date.getUTCFullYear()),fill:"rev1",inset:1}), Plot.text(observed, {x: d => d.date.getUTCMonth() +1,y: d =>String(d.date.getUTCFullYear()),text: d =>fmtS(d.rev1),fontSize:10,fill: d =>Math.abs(d.rev1) >12?"#faf7f0":"#29251f"}) ]})
The same revisions arranged by month and year. Oxblood marks an upward revision, blue a downward one.
Follow one month
Pick a reference month and see every number the NYPD has published for it, and when.
viewof monthSel = Inputs.select(observedMonths, {label:"Reference month",format: d =>fmtM(parseM(d)),value: observedMonths[observedMonths.length-1]})
The year-over-year change the NYPD announces uses the new month’s first count against the prior year’s already-audited count. Recomputing both sides at one year gives the audited comparison.
yoyType = ["Total Index","Transit Crime"].includes(ct) ? ct :"Total Index"yoy = data.yoy.filter(d => d.type=== yoyType && d.audited!=null&& d.announced!=null).map(d => ({...d,date:parseM(d.m)}))flips = yoy.filter(d => d.flip)html`<div class="kpis"> <div class="kpi"><div class="v">${flips.length}<span class="u">of ${yoy.length}</span></div><div class="l">announced year-over-year changes that reversed sign once audited<br><span class="meta">${yoyType ==="Total Index"?"seven major felonies":"transit crime"}${["Total Index","Transit Crime"].includes(ct) ?"":" (shown for the index; per-type series are too small for a fair test)"}</span></div></div> <div class="kpi"><div class="v">${flips.filter(d => d.announced<0&& d.audited>0).length}</div><div class="l">of those were announced as declines and audited as increases</div></div></div>`
Plot.plot({ width,height:240,marginLeft:50,style: {fontFamily:"inherit",fontSize:"13px",background:"transparent",color:"#29251f"},x: {label:null},y: {label:"Year-over-year change (%)",grid:true,tickFormat: d => d3.format("+d")(d)},color: {domain: ["Announced","Audited"],range: ["#a89a7b","#7a2e2e"],legend:true},marks: [ Plot.ruleY([0]), Plot.link(yoy, {x:"date",y1:"announced",y2:"audited",stroke: d => d.flip?"#7a2e2e":"#cfbf9f",strokeWidth: d => d.flip?2:1}), Plot.dot(yoy, {x:"date",y:"announced",fill:"#a89a7b",r:3}), Plot.dot(yoy, {x:"date",y:"audited",fill:"#7a2e2e",r:3}), Plot.tip(yoy, Plot.pointerX({x:"date",y:"audited",title: d =>`${fmtM(d.date)}\nAnnounced ${fmtS(d.announced)}% · audited ${fmtS(d.audited)}%${d.flip?"\nSign reversed":""}`})) ]})
Each line joins the year-over-year change as announced to the same change recomputed once both months had been audited.
Transit vs. citywide
gap = data.gap.map(d => ({...d,date:parseM(d.m)}))gapLong = gap.flatMap(d => [{date: d.date,who:"Transit crime",v: d.transit_roll}, {date: d.date,who:"Citywide index",v: d.index_roll}]).filter(d => d.v!=null)Plot.plot({ width,height:240,marginLeft:50,style: {fontFamily:"inherit",fontSize:"13px",background:"transparent",color:"#29251f"},x: {label:null},y: {label:"Revision at one year (%), six-month trailing mean",grid:true,tickFormat: d => d3.format("+d")(d)},color: {domain: ["Transit crime","Citywide index"],range: ["#7a2e2e","#29251f"],legend:true},marks: [ Plot.ruleY([0]), Plot.dot(gap.filter(d => d.transit!=null), {x:"date",y:"transit",fill:"#7a2e2e",fillOpacity:0.25,r:2}), Plot.lineY(gapLong, {x:"date",y:"v",stroke:"who",strokeWidth:1.8}), Plot.tip(gap.filter(d => d.gap_pp!=null), Plot.pointerX({x:"date",y:"transit",title: d =>`${fmtM(d.date)}\nTransit ${fmtS(d.transit)}% · citywide ${fmtS(d.index)}% · gap ${fmtS(d.gap_pp)} pts`})) ]})
Dots are individual months for transit crime; lines are six-month trailing means.
What is counted. Each value is a count of complaints for one calendar month as printed in an NYPD publication: the monthly crime press release (first announcement and the anniversary release twelve months later) and the NYPD crime dashboard. “One year later” is the anniversary press release; “latest” is the most recent publication of any kind. The seven major felonies are murder, rape, robbery, felony assault, burglary, grand larceny, and grand larceny auto; the index total is their sum where all seven are printed. Reference months since January 2018; publications through .
Why the numbers change. Complaint reports are audited and reclassified after the month closes. Under report-date counting the ledger closes with the month, so revisions are upgrades of reports already on file, not late arrivals, and they run almost entirely upward.
Limitations. The one-year figure is itself a re-report, not ground truth; a few months have no anniversary release because the NYPD framed that month’s release differently. Announced year-over-year changes are recomputed from the counts printed in the same release, which can differ by a tenth of a point from the percentage the release states.
Hall, J. (2026). NYC Crime Data Vintages. https://johnjhallresearch.com/crime. Accessed [date].
Publications through · page built · vintages, press releases