[{"data":1,"prerenderedAt":2439},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-gaps-in-a-numbered-sequence-with-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-gaps-in-a-numbered-sequence-with-pandas":2405,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-gaps-in-a-numbered-sequence-with-pandas":2414,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-gaps-in-a-numbered-sequence-with-pandas":2419,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-gaps-in-a-numbered-sequence-with-pandas":2420},{"id":4,"title":5,"body":6,"dateModified":2371,"datePublished":2371,"description":2372,"extension":2373,"faq":2374,"keyword":2383,"kind":2384,"meta":2385,"navigation":283,"path":2394,"seo":2395,"slug":2398,"stem":2399,"topics":2400,"type":2403,"__hash__":2404},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-gaps-in-a-numbered-sequence-with-pandas\u002Findex.md","Find Gaps in a Numbered Sequence with pandas",{"type":7,"value":8,"toc":2358},"minimark",[9,19,24,53,71,248,252,255,508,519,523,526,698,705,708,903,906,910,917,1120,1226,1230,1233,1347,1354,1358,1361,1484,1487,1615,1619,1622,2210,2213,2217,2268,2272,2279,2283,2289,2295,2308,2314,2318,2354],[10,11,12,13,18],"p",{},"Auditors ask for it, tax authorities in many countries require it, and it catches real problems: every number in a sequence of invoices, credit notes, cheques or delivery notes should be accounted for. A gap can be a voided document nobody recorded, an export that missed rows, or an invoice raised outside the system. In Excel people sort the column and scan it by eye, which works until the list has twenty thousand rows and three numbering series. pandas finds every gap, duplicate and out-of-order number in a few lines. This page is part of ",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002F","Validating Excel Data with Python",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47,50],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," pandas",[35,51,52],{"class":45}," openpyxl\n",[10,54,55,58,59,62,63,66,67,70],{},[32,56,57],{},"documents.xlsx"," is a document register exported from an accounting system: document number, issue date and amount, in no particular order. A second file, ",[32,60,61],{},"void_register.xlsx",", lists documents that were voided after their number was issued. The register contains three numbering series: sales invoices for 2026 (",[32,64,65],{},"INV-2026-000001"," onwards), credit notes for 2026 (",[32,68,69],{},"CN-2026-…","), and the end of the 2025 invoice series, whose numbers continue from 3801. Each series must be checked on its own: the credit-note numbers are not gaps in the invoice sequence.",[72,73,81,82,81,86,81,90,81,98,81,108,81,119,81,126,81,130,81,135,81,141,81,146,81,150,81,157,81,163,81,167,81,171,81,174,81,177,81,183,81,187,81,190,81,194,81,197,81,202,81,209,81,214,81,218,81,221,81,225,81,228,81,233,81,240,81,245],"svg",{"viewBox":74,"role":75,"ariaLabelledBy":76,"xmlns":79,"style":80},"4 0 752 142","img",[77,78],"sq-flow-t","sq-flow-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[83,84,85],"title",{"id":77},"From document IDs to exceptions",[87,88,89],"desc",{"id":78},"Document IDs are split into a series prefix and a number. Each series is checked separately for missing numbers, duplicates and numbers whose issue date is earlier than a lower number's. The findings go to an exceptions report.",[91,92],"rect",{"x":93,"y":94,"width":95,"height":96,"fill":97},"4","0","752","142","#ffffff",[91,99],{"x":100,"y":101,"width":102,"height":103,"rx":104,"fill":105,"stroke":106,"style":107},"20.0","16.0","115.2","110.5","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[109,110,112],"g",{"transform":111},"translate(65.6,29.0) scale(1.000)",[113,114],"path",{"d":115,"fill":116,"stroke":117,"style":118},"M6 3h9l4 4v14H6z M15 3v4h4","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[120,121,125],"text",{"x":122,"y":123,"style":124},"77.6","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Document",[120,127,129],{"x":122,"y":128,"style":124},"93.0","IDs",[120,131,134],{"x":122,"y":132,"style":133},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","INV-2026-000123",[37,136],{"x1":137,"y1":138,"x2":139,"y2":138,"stroke":140,"style":107},"140.2","71.2","162.2","var(--brand,#5b5cf0)",[142,143],"polygon",{"points":144,"fill":145},"166.2,71.2 157.2,76.2 157.2,66.2","#5b5cf0",[91,147],{"x":148,"y":101,"width":102,"height":103,"rx":104,"fill":149,"stroke":140,"style":107},"171.2","#f0f4ff",[109,151,153],{"transform":152},"translate(216.8,37.8) scale(1.000)",[113,154],{"d":155,"fill":116,"stroke":156,"style":118},"M4 12h6 M10 12l6-6h4 M10 12l6 6h4 M17 3l3 3-3 3 M17 15l3 3-3 3","var(--brand-strong,#4338ca)",[120,158,162],{"x":159,"y":160,"style":161},"228.8","84.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Split",[120,164,166],{"x":159,"y":165,"style":133},"103.8","series + number",[37,168],{"x1":169,"y1":138,"x2":170,"y2":138,"stroke":140,"style":107},"291.4","313.4",[142,172],{"points":173,"fill":145},"317.4,71.2 308.4,76.2 308.4,66.2",[91,175],{"x":176,"y":101,"width":102,"height":103,"rx":104,"fill":149,"stroke":140,"style":107},"322.4",[109,178,180],{"transform":179},"translate(368.0,37.8) scale(1.000)",[113,181],{"d":182,"fill":116,"stroke":156,"style":118},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5",[120,184,186],{"x":185,"y":160,"style":161},"380.0","Per series",[120,188,189],{"x":185,"y":165,"style":133},"groupby",[37,191],{"x1":192,"y1":138,"x2":193,"y2":138,"stroke":140,"style":107},"442.6","464.6",[142,195],{"points":196,"fill":145},"468.6,71.2 459.6,76.2 459.6,66.2",[91,198],{"x":199,"y":101,"width":102,"height":103,"rx":104,"fill":200,"stroke":201,"style":107},"473.6","#d9f4f1","var(--teal,#0f9488)",[109,203,205],{"transform":204},"translate(519.2,29.0) scale(1.000)",[113,206],{"d":207,"fill":116,"stroke":208,"style":118},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5","var(--teal-ink,#0b6157)",[120,210,213],{"x":211,"y":123,"style":212},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Checks",[120,215,217],{"x":211,"y":216,"style":133},"95.0","gaps, dupes,",[120,219,220],{"x":211,"y":132,"style":133},"order",[37,222],{"x1":223,"y1":138,"x2":224,"y2":138,"stroke":140,"style":107},"593.8","615.8",[142,226],{"points":227,"fill":145},"619.8,71.2 610.8,76.2 610.8,66.2",[91,229],{"x":230,"y":101,"width":102,"height":103,"rx":104,"fill":231,"stroke":232,"style":107},"624.8","#fdefd8","var(--gold,#b4740a)",[109,234,236],{"transform":235},"translate(670.4,37.8) scale(1.000)",[113,237],{"d":238,"fill":116,"stroke":239,"style":118},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)",[120,241,244],{"x":242,"y":160,"style":243},"682.4","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Exceptions",[120,246,247],{"x":242,"y":165,"style":133},"report",[20,249,251],{"id":250},"step-1-split-the-series-and-the-number","Step 1 — Split the series and the number",[10,253,254],{},"The number is the digits at the end; the series is everything before them. One regular expression with two named groups does both, and fails loudly on IDs that do not fit the pattern:",[25,256,260],{"className":257,"code":258,"language":259,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\ndocs = pd.read_excel(\"documents.xlsx\", parse_dates=[\"IssueDate\"], dtype={\"DocumentNo\": str})\nparts = docs[\"DocumentNo\"].str.extract(r\"^(?P\u003CSeries>.*?)(?P\u003CNumber>\\d+)$\")\nbad = docs[parts[\"Number\"].isna()]\nassert bad.empty, f\"IDs without a trailing number: {bad['DocumentNo'].tolist()}\"\ndocs = docs.join(parts.assign(Number=parts[\"Number\"].astype(int)))\nprint(docs.groupby(\"Series\")[\"Number\"].agg([\"count\", \"min\", \"max\"]).to_string())\n","python",[32,261,262,278,285,339,393,410,443,472],{"__ignoreMap":30},[35,263,264,268,272,275],{"class":37,"line":38},[35,265,267],{"class":266},"s-kum","import",[35,269,271],{"class":270},"skGVy"," pandas ",[35,273,274],{"class":266},"as",[35,276,277],{"class":270}," pd\n",[35,279,281],{"class":37,"line":280},2,[35,282,284],{"emptyLinePlaceholder":283},true,"\n",[35,286,288,291,294,297,300,303,307,309,312,315,318,321,323,326,329,332,336],{"class":37,"line":287},3,[35,289,290],{"class":270},"docs ",[35,292,293],{"class":266},"=",[35,295,296],{"class":270}," pd.read_excel(",[35,298,299],{"class":45},"\"documents.xlsx\"",[35,301,302],{"class":270},", ",[35,304,306],{"class":305},"sa561","parse_dates",[35,308,293],{"class":266},[35,310,311],{"class":270},"[",[35,313,314],{"class":45},"\"IssueDate\"",[35,316,317],{"class":270},"], ",[35,319,320],{"class":305},"dtype",[35,322,293],{"class":266},[35,324,325],{"class":270},"{",[35,327,328],{"class":45},"\"DocumentNo\"",[35,330,331],{"class":270},": ",[35,333,335],{"class":334},"sP0c6","str",[35,337,338],{"class":270},"})\n",[35,340,342,345,347,350,352,355,358,361,364,368,370,373,376,379,382,385,388,390],{"class":37,"line":341},4,[35,343,344],{"class":270},"parts ",[35,346,293],{"class":266},[35,348,349],{"class":270}," docs[",[35,351,328],{"class":45},[35,353,354],{"class":270},"].str.extract(",[35,356,357],{"class":266},"r",[35,359,360],{"class":45},"\"",[35,362,363],{"class":334},"^(",[35,365,367],{"class":366},"sx1sP","?P\u003CSeries>",[35,369,18],{"class":334},[35,371,372],{"class":266},"*?",[35,374,375],{"class":334},")(",[35,377,378],{"class":366},"?P\u003CNumber>",[35,380,381],{"class":334},"\\d",[35,383,384],{"class":266},"+",[35,386,387],{"class":334},")$",[35,389,360],{"class":45},[35,391,392],{"class":270},")\n",[35,394,396,399,401,404,407],{"class":37,"line":395},5,[35,397,398],{"class":270},"bad ",[35,400,293],{"class":266},[35,402,403],{"class":270}," docs[parts[",[35,405,406],{"class":45},"\"Number\"",[35,408,409],{"class":270},"].isna()]\n",[35,411,413,416,419,422,425,428,431,434,437,440],{"class":37,"line":412},6,[35,414,415],{"class":266},"assert",[35,417,418],{"class":270}," bad.empty, ",[35,420,421],{"class":266},"f",[35,423,424],{"class":45},"\"IDs without a trailing number: ",[35,426,325],{"class":427},"sSjpA",[35,429,430],{"class":270},"bad[",[35,432,433],{"class":45},"'DocumentNo'",[35,435,436],{"class":270},"].tolist()",[35,438,439],{"class":427},"}",[35,441,442],{"class":45},"\"\n",[35,444,446,448,450,453,456,458,461,463,466,469],{"class":37,"line":445},7,[35,447,290],{"class":270},[35,449,293],{"class":266},[35,451,452],{"class":270}," docs.join(parts.assign(",[35,454,455],{"class":305},"Number",[35,457,293],{"class":266},[35,459,460],{"class":270},"parts[",[35,462,406],{"class":45},[35,464,465],{"class":270},"].astype(",[35,467,468],{"class":334},"int",[35,470,471],{"class":270},")))\n",[35,473,475,478,481,484,487,489,492,495,497,500,502,505],{"class":37,"line":474},8,[35,476,477],{"class":334},"print",[35,479,480],{"class":270},"(docs.groupby(",[35,482,483],{"class":45},"\"Series\"",[35,485,486],{"class":270},")[",[35,488,406],{"class":45},[35,490,491],{"class":270},"].agg([",[35,493,494],{"class":45},"\"count\"",[35,496,302],{"class":270},[35,498,499],{"class":45},"\"min\"",[35,501,302],{"class":270},[35,503,504],{"class":45},"\"max\"",[35,506,507],{"class":270},"]).to_string())\n",[10,509,510,511,514,515,518],{},"Reading the ID column as text matters. A column of plain numbers such as ",[32,512,513],{},"000123"," read as integers loses its leading zeros, and a mixed column can come back as floats, where ",[32,516,517],{},"123.0"," breaks the extraction.",[20,520,522],{"id":521},"step-2-missing-numbers-per-series","Step 2 — Missing numbers per series",[10,524,525],{},"For each series, the numbers that should exist are every integer from the lowest to the highest; the missing ones are the difference between that range and the numbers present. Sets make the difference explicit:",[25,527,529],{"className":257,"code":528,"language":259,"meta":30,"style":30},"def missing_numbers(numbers):\n    present = set(numbers)\n    return sorted(set(range(min(present), max(present) + 1)) - present)\n\ngaps = {series: missing_numbers(g[\"Number\"]) for series, g in docs.groupby(\"Series\")}\nfor series, missing in gaps.items():\n    print(f\"{series:10} missing {len(missing)}: {missing}\")\n",[32,530,531,543,556,603,607,639,651],{"__ignoreMap":30},[35,532,533,536,540],{"class":37,"line":38},[35,534,535],{"class":266},"def",[35,537,539],{"class":538},"s_Opv"," missing_numbers",[35,541,542],{"class":270},"(numbers):\n",[35,544,545,548,550,553],{"class":37,"line":280},[35,546,547],{"class":270},"    present ",[35,549,293],{"class":266},[35,551,552],{"class":334}," set",[35,554,555],{"class":270},"(numbers)\n",[35,557,558,561,564,567,570,572,575,577,580,583,586,589,591,594,597,600],{"class":37,"line":287},[35,559,560],{"class":266},"    return",[35,562,563],{"class":334}," sorted",[35,565,566],{"class":270},"(",[35,568,569],{"class":334},"set",[35,571,566],{"class":270},[35,573,574],{"class":334},"range",[35,576,566],{"class":270},[35,578,579],{"class":334},"min",[35,581,582],{"class":270},"(present), ",[35,584,585],{"class":334},"max",[35,587,588],{"class":270},"(present) ",[35,590,384],{"class":266},[35,592,593],{"class":334}," 1",[35,595,596],{"class":270},")) ",[35,598,599],{"class":266},"-",[35,601,602],{"class":270}," present)\n",[35,604,605],{"class":37,"line":341},[35,606,284],{"emptyLinePlaceholder":283},[35,608,609,612,614,617,619,622,625,628,631,634,636],{"class":37,"line":395},[35,610,611],{"class":270},"gaps ",[35,613,293],{"class":266},[35,615,616],{"class":270}," {series: missing_numbers(g[",[35,618,406],{"class":45},[35,620,621],{"class":270},"]) ",[35,623,624],{"class":266},"for",[35,626,627],{"class":270}," series, g ",[35,629,630],{"class":266},"in",[35,632,633],{"class":270}," docs.groupby(",[35,635,483],{"class":45},[35,637,638],{"class":270},")}\n",[35,640,641,643,646,648],{"class":37,"line":412},[35,642,624],{"class":266},[35,644,645],{"class":270}," series, missing ",[35,647,630],{"class":266},[35,649,650],{"class":270}," gaps.items():\n",[35,652,653,656,658,660,662,664,667,670,672,675,677,680,683,685,687,689,692,694,696],{"class":37,"line":445},[35,654,655],{"class":334},"    print",[35,657,566],{"class":270},[35,659,421],{"class":266},[35,661,360],{"class":45},[35,663,325],{"class":427},[35,665,666],{"class":270},"series",[35,668,669],{"class":266},":10",[35,671,439],{"class":427},[35,673,674],{"class":45}," missing ",[35,676,325],{"class":427},[35,678,679],{"class":334},"len",[35,681,682],{"class":270},"(missing)",[35,684,439],{"class":427},[35,686,331],{"class":45},[35,688,325],{"class":427},[35,690,691],{"class":270},"missing",[35,693,439],{"class":427},[35,695,360],{"class":45},[35,697,392],{"class":270},[10,699,700,701,704],{},"The range starts at the lowest number present, which is right for a series that continues from last year like ",[32,702,703],{},"INV-2025-",". For a series that must start at 1, check the start explicitly: a missing first invoice is also a gap.",[10,706,707],{},"Some of those gaps are already explained. Numbers voided in the accounting system do not appear in the register export but are recorded in the void register; remove them before anyone investigates:",[25,709,711],{"className":257,"code":710,"language":259,"meta":30,"style":30},"voids = pd.read_excel(\"void_register.xlsx\", dtype={\"DocumentNo\": str})\nvparts = voids[\"DocumentNo\"].str.extract(r\"^(?P\u003CSeries>.*?)(?P\u003CNumber>\\d+)$\")\nvoided = {(s, int(n)) for s, n in vparts.itertuples(index=False)}\nunexplained = {s: [n for n in m if (s, n) not in voided] for s, m in gaps.items()}\nfor series, missing in unexplained.items():\n    print(f\"{series:10} unexplained {missing}\")\n",[32,712,713,741,781,816,861,872],{"__ignoreMap":30},[35,714,715,718,720,722,725,727,729,731,733,735,737,739],{"class":37,"line":38},[35,716,717],{"class":270},"voids ",[35,719,293],{"class":266},[35,721,296],{"class":270},[35,723,724],{"class":45},"\"void_register.xlsx\"",[35,726,302],{"class":270},[35,728,320],{"class":305},[35,730,293],{"class":266},[35,732,325],{"class":270},[35,734,328],{"class":45},[35,736,331],{"class":270},[35,738,335],{"class":334},[35,740,338],{"class":270},[35,742,743,746,748,751,753,755,757,759,761,763,765,767,769,771,773,775,777,779],{"class":37,"line":280},[35,744,745],{"class":270},"vparts ",[35,747,293],{"class":266},[35,749,750],{"class":270}," voids[",[35,752,328],{"class":45},[35,754,354],{"class":270},[35,756,357],{"class":266},[35,758,360],{"class":45},[35,760,363],{"class":334},[35,762,367],{"class":366},[35,764,18],{"class":334},[35,766,372],{"class":266},[35,768,375],{"class":334},[35,770,378],{"class":366},[35,772,381],{"class":334},[35,774,384],{"class":266},[35,776,387],{"class":334},[35,778,360],{"class":45},[35,780,392],{"class":270},[35,782,783,786,788,791,793,796,798,801,803,806,809,811,814],{"class":37,"line":287},[35,784,785],{"class":270},"voided ",[35,787,293],{"class":266},[35,789,790],{"class":270}," {(s, ",[35,792,468],{"class":334},[35,794,795],{"class":270},"(n)) ",[35,797,624],{"class":266},[35,799,800],{"class":270}," s, n ",[35,802,630],{"class":266},[35,804,805],{"class":270}," vparts.itertuples(",[35,807,808],{"class":305},"index",[35,810,293],{"class":266},[35,812,813],{"class":334},"False",[35,815,638],{"class":270},[35,817,818,821,823,826,828,831,833,836,839,842,845,848,851,853,856,858],{"class":37,"line":341},[35,819,820],{"class":270},"unexplained ",[35,822,293],{"class":266},[35,824,825],{"class":270}," {s: [n ",[35,827,624],{"class":266},[35,829,830],{"class":270}," n ",[35,832,630],{"class":266},[35,834,835],{"class":270}," m ",[35,837,838],{"class":266},"if",[35,840,841],{"class":270}," (s, n) ",[35,843,844],{"class":266},"not",[35,846,847],{"class":266}," in",[35,849,850],{"class":270}," voided] ",[35,852,624],{"class":266},[35,854,855],{"class":270}," s, m ",[35,857,630],{"class":266},[35,859,860],{"class":270}," gaps.items()}\n",[35,862,863,865,867,869],{"class":37,"line":395},[35,864,624],{"class":266},[35,866,645],{"class":270},[35,868,630],{"class":266},[35,870,871],{"class":270}," unexplained.items():\n",[35,873,874,876,878,880,882,884,886,888,890,893,895,897,899,901],{"class":37,"line":412},[35,875,655],{"class":334},[35,877,566],{"class":270},[35,879,421],{"class":266},[35,881,360],{"class":45},[35,883,325],{"class":427},[35,885,666],{"class":270},[35,887,669],{"class":266},[35,889,439],{"class":427},[35,891,892],{"class":45}," unexplained ",[35,894,325],{"class":427},[35,896,691],{"class":270},[35,898,439],{"class":427},[35,900,360],{"class":45},[35,902,392],{"class":270},[10,904,905],{},"That leaves four unexplained numbers in the 2026 invoice series and none in the credit notes. From here on the report works with the unexplained gaps. Keep the void register as the source of truth for voids, rather than a list typed into the script, so the check stays correct as new documents are voided.",[20,907,909],{"id":908},"step-3-report-gaps-as-ranges","Step 3 — Report gaps as ranges",[10,911,912,913,916],{},"A list of individual numbers gets long when a whole block is missing. Collapse consecutive numbers into ranges, the way an auditor would write them. Consecutive runs share the same value of ",[32,914,915],{},"number − position",":",[25,918,920],{"className":257,"code":919,"language":259,"meta":30,"style":30},"def as_ranges(numbers):\n    if not numbers:\n        return []\n    s = pd.Series(numbers)\n    runs = s.groupby(s - pd.RangeIndex(len(s))).agg([\"min\", \"max\"])\n    return [f\"{a}\" if a == b else f\"{a}–{b}\" for a, b in runs.itertuples(index=False)]\n\nfor series, missing in unexplained.items():\n    print(f\"{series:10} {', '.join(as_ranges(missing)) or 'no gaps'}\")\n",[32,921,922,931,942,950,960,989,1066,1070,1080],{"__ignoreMap":30},[35,923,924,926,929],{"class":37,"line":38},[35,925,535],{"class":266},[35,927,928],{"class":538}," as_ranges",[35,930,542],{"class":270},[35,932,933,936,939],{"class":37,"line":280},[35,934,935],{"class":266},"    if",[35,937,938],{"class":266}," not",[35,940,941],{"class":270}," numbers:\n",[35,943,944,947],{"class":37,"line":287},[35,945,946],{"class":266},"        return",[35,948,949],{"class":270}," []\n",[35,951,952,955,957],{"class":37,"line":341},[35,953,954],{"class":270},"    s ",[35,956,293],{"class":266},[35,958,959],{"class":270}," pd.Series(numbers)\n",[35,961,962,965,967,970,972,975,977,980,982,984,986],{"class":37,"line":395},[35,963,964],{"class":270},"    runs ",[35,966,293],{"class":266},[35,968,969],{"class":270}," s.groupby(s ",[35,971,599],{"class":266},[35,973,974],{"class":270}," pd.RangeIndex(",[35,976,679],{"class":334},[35,978,979],{"class":270},"(s))).agg([",[35,981,499],{"class":45},[35,983,302],{"class":270},[35,985,504],{"class":45},[35,987,988],{"class":270},"])\n",[35,990,991,993,996,998,1000,1002,1004,1006,1008,1011,1014,1017,1020,1023,1026,1028,1030,1032,1034,1037,1039,1042,1044,1046,1049,1052,1054,1057,1059,1061,1063],{"class":37,"line":412},[35,992,560],{"class":266},[35,994,995],{"class":270}," [",[35,997,421],{"class":266},[35,999,360],{"class":45},[35,1001,325],{"class":427},[35,1003,14],{"class":270},[35,1005,439],{"class":427},[35,1007,360],{"class":45},[35,1009,1010],{"class":266}," if",[35,1012,1013],{"class":270}," a ",[35,1015,1016],{"class":266},"==",[35,1018,1019],{"class":270}," b ",[35,1021,1022],{"class":266},"else",[35,1024,1025],{"class":266}," f",[35,1027,360],{"class":45},[35,1029,325],{"class":427},[35,1031,14],{"class":270},[35,1033,439],{"class":427},[35,1035,1036],{"class":45},"–",[35,1038,325],{"class":427},[35,1040,1041],{"class":270},"b",[35,1043,439],{"class":427},[35,1045,360],{"class":45},[35,1047,1048],{"class":266}," for",[35,1050,1051],{"class":270}," a, b ",[35,1053,630],{"class":266},[35,1055,1056],{"class":270}," runs.itertuples(",[35,1058,808],{"class":305},[35,1060,293],{"class":266},[35,1062,813],{"class":334},[35,1064,1065],{"class":270},")]\n",[35,1067,1068],{"class":37,"line":445},[35,1069,284],{"emptyLinePlaceholder":283},[35,1071,1072,1074,1076,1078],{"class":37,"line":474},[35,1073,624],{"class":266},[35,1075,645],{"class":270},[35,1077,630],{"class":266},[35,1079,871],{"class":270},[35,1081,1083,1085,1087,1089,1091,1093,1095,1097,1099,1102,1105,1108,1111,1114,1116,1118],{"class":37,"line":1082},9,[35,1084,655],{"class":334},[35,1086,566],{"class":270},[35,1088,421],{"class":266},[35,1090,360],{"class":45},[35,1092,325],{"class":427},[35,1094,666],{"class":270},[35,1096,669],{"class":266},[35,1098,439],{"class":427},[35,1100,1101],{"class":427}," {",[35,1103,1104],{"class":45},"', '",[35,1106,1107],{"class":270},".join(as_ranges(missing)) ",[35,1109,1110],{"class":266},"or",[35,1112,1113],{"class":45}," 'no gaps'",[35,1115,439],{"class":427},[35,1117,360],{"class":45},[35,1119,392],{"class":270},[72,1121,81,1127,81,1130,81,1133,81,1138,81,1145,81,1150,81,1154,81,1159,81,1162,81,1167,81,1170,81,1174,81,1177,81,1181,81,1185,81,1189,81,1193,81,1197,81,1202,81,1205,81,1209,81,1212,81,1220],{"viewBox":1122,"role":75,"ariaLabelledBy":1123,"xmlns":79,"style":1126},"192 0 363 196",[1124,1125],"sq-ranges-t","sq-ranges-d","width:100%;max-width:363px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[83,1128,1129],{"id":1124},"Missing numbers collapsed into ranges",[87,1131,1132],{"id":1125},"On the left the sorted numbers present around the first gap, 55, 56, 60, 61. On the right the missing numbers 57, 58 and 59 reported as the single range 57 to 59.",[91,1134],{"x":1135,"y":94,"width":1136,"height":1137,"fill":97},"192","363","196",[91,1139],{"x":1140,"y":1141,"width":1142,"height":1143,"rx":94,"fill":149,"stroke":140,"style":1144},"228.6","40.0","69.4","28.0","stroke-width:1.5px",[120,1146,455],{"x":1147,"y":1148,"style":1149},"263.3","59.0","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[91,1151],{"x":1140,"y":1152,"width":1142,"height":1143,"rx":94,"fill":97,"stroke":106,"style":1153},"68.0","stroke-width:1px",[120,1155,1158],{"x":1147,"y":1156,"style":1157},"87.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","55",[91,1160],{"x":1140,"y":1161,"width":1142,"height":1143,"rx":94,"fill":231,"stroke":232,"style":1144},"96.0",[120,1163,1166],{"x":1147,"y":1164,"style":1165},"115.0","font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","56",[91,1168],{"x":1140,"y":1169,"width":1142,"height":1143,"rx":94,"fill":231,"stroke":232,"style":1144},"124.0",[120,1171,1173],{"x":1147,"y":1172,"style":1165},"143.0","60",[91,1175],{"x":1140,"y":1176,"width":1142,"height":1143,"rx":94,"fill":97,"stroke":106,"style":1153},"152.0",[120,1178,1180],{"x":1147,"y":1179,"style":1157},"171.0","61",[91,1182],{"x":1183,"y":1141,"width":1184,"height":1143,"rx":94,"fill":149,"stroke":140,"style":1144},"462.9","68.5",[120,1186,1188],{"x":1187,"y":1148,"style":1149},"497.2","Missing",[91,1190],{"x":1183,"y":1152,"width":1184,"height":1143,"rx":94,"fill":1191,"stroke":1192,"style":1144},"#fce9e9","var(--danger,#dc2626)",[120,1194,1196],{"x":1187,"y":1156,"style":1195},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","57–59",[120,1198,1201],{"x":1147,"y":1199,"style":1200},"29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","numbers present",[120,1203,1204],{"x":1187,"y":1199,"style":1200},"reported gap",[37,1206],{"x1":1207,"y1":1152,"x2":1208,"y2":1152,"stroke":140,"style":107},"308.0","448.9",[142,1210],{"points":1211,"fill":145},"452.9,68.0 443.9,73.0 443.9,63.0",[91,1213],{"x":1214,"y":1215,"width":1216,"height":1217,"rx":1218,"fill":97,"stroke":106,"style":1219},"330.5","35.0","100.0","22.0","11","stroke-width:1.2px",[120,1221,1225],{"x":1222,"y":1223,"style":1224},"380.5","50.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","as_ranges()",[20,1227,1229],{"id":1228},"step-4-duplicates","Step 4 — Duplicates",[10,1231,1232],{},"A number used twice is as serious as one missing: it can mean a document was re-issued with the same number, or the export duplicated a row. Separate the two by checking whether the duplicate rows are identical:",[25,1234,1236],{"className":257,"code":1235,"language":259,"meta":30,"style":30},"dupes = docs[docs.duplicated([\"Series\", \"Number\"], keep=False)].sort_values(\"DocumentNo\")\nprint(dupes[[\"DocumentNo\", \"IssueDate\", \"Amount\"]].to_string(index=False))\nidentical = dupes.duplicated([\"DocumentNo\", \"IssueDate\", \"Amount\"], keep=False)\nprint(\"identical rows (likely an export duplicate):\", bool(identical.all()))\n",[32,1237,1238,1270,1300,1330],{"__ignoreMap":30},[35,1239,1240,1243,1245,1248,1250,1252,1254,1256,1259,1261,1263,1266,1268],{"class":37,"line":38},[35,1241,1242],{"class":270},"dupes ",[35,1244,293],{"class":266},[35,1246,1247],{"class":270}," docs[docs.duplicated([",[35,1249,483],{"class":45},[35,1251,302],{"class":270},[35,1253,406],{"class":45},[35,1255,317],{"class":270},[35,1257,1258],{"class":305},"keep",[35,1260,293],{"class":266},[35,1262,813],{"class":334},[35,1264,1265],{"class":270},")].sort_values(",[35,1267,328],{"class":45},[35,1269,392],{"class":270},[35,1271,1272,1274,1277,1279,1281,1283,1285,1288,1291,1293,1295,1297],{"class":37,"line":280},[35,1273,477],{"class":334},[35,1275,1276],{"class":270},"(dupes[[",[35,1278,328],{"class":45},[35,1280,302],{"class":270},[35,1282,314],{"class":45},[35,1284,302],{"class":270},[35,1286,1287],{"class":45},"\"Amount\"",[35,1289,1290],{"class":270},"]].to_string(",[35,1292,808],{"class":305},[35,1294,293],{"class":266},[35,1296,813],{"class":334},[35,1298,1299],{"class":270},"))\n",[35,1301,1302,1305,1307,1310,1312,1314,1316,1318,1320,1322,1324,1326,1328],{"class":37,"line":287},[35,1303,1304],{"class":270},"identical ",[35,1306,293],{"class":266},[35,1308,1309],{"class":270}," dupes.duplicated([",[35,1311,328],{"class":45},[35,1313,302],{"class":270},[35,1315,314],{"class":45},[35,1317,302],{"class":270},[35,1319,1287],{"class":45},[35,1321,317],{"class":270},[35,1323,1258],{"class":305},[35,1325,293],{"class":266},[35,1327,813],{"class":334},[35,1329,392],{"class":270},[35,1331,1332,1334,1336,1339,1341,1344],{"class":37,"line":341},[35,1333,477],{"class":334},[35,1335,566],{"class":270},[35,1337,1338],{"class":45},"\"identical rows (likely an export duplicate):\"",[35,1340,302],{"class":270},[35,1342,1343],{"class":334},"bool",[35,1345,1346],{"class":270},"(identical.all()))\n",[10,1348,1349,1350,1353],{},"Here the two rows for ",[32,1351,1352],{},"INV-2026-000143"," have different dates and amounts, so they are two different documents sharing a number, which needs investigating in the source system rather than deleting.",[20,1355,1357],{"id":1356},"step-5-numbers-issued-out-of-order","Step 5 — Numbers issued out of order",[10,1359,1360],{},"In a sequential series, a higher number should never carry an earlier date than a lower one. A document that breaks that rule was often created later and backdated, or numbered by hand. Sort each series by number and compare every date with the latest date seen so far:",[25,1362,1364],{"className":257,"code":1363,"language":259,"meta":30,"style":30},"seq = docs.sort_values([\"Series\", \"Number\"]).copy()\nseq[\"LatestSoFar\"] = seq.groupby(\"Series\")[\"IssueDate\"].cummax()\nseq[\"PrevLatest\"] = seq.groupby(\"Series\")[\"LatestSoFar\"].shift()\nbackdated = seq[seq[\"IssueDate\"] \u003C seq[\"PrevLatest\"]]\nprint(backdated[[\"DocumentNo\", \"IssueDate\", \"PrevLatest\"]].to_string(index=False))\n",[32,1365,1366,1385,1410,1432,1457],{"__ignoreMap":30},[35,1367,1368,1371,1373,1376,1378,1380,1382],{"class":37,"line":38},[35,1369,1370],{"class":270},"seq ",[35,1372,293],{"class":266},[35,1374,1375],{"class":270}," docs.sort_values([",[35,1377,483],{"class":45},[35,1379,302],{"class":270},[35,1381,406],{"class":45},[35,1383,1384],{"class":270},"]).copy()\n",[35,1386,1387,1390,1393,1396,1398,1401,1403,1405,1407],{"class":37,"line":280},[35,1388,1389],{"class":270},"seq[",[35,1391,1392],{"class":45},"\"LatestSoFar\"",[35,1394,1395],{"class":270},"] ",[35,1397,293],{"class":266},[35,1399,1400],{"class":270}," seq.groupby(",[35,1402,483],{"class":45},[35,1404,486],{"class":270},[35,1406,314],{"class":45},[35,1408,1409],{"class":270},"].cummax()\n",[35,1411,1412,1414,1417,1419,1421,1423,1425,1427,1429],{"class":37,"line":287},[35,1413,1389],{"class":270},[35,1415,1416],{"class":45},"\"PrevLatest\"",[35,1418,1395],{"class":270},[35,1420,293],{"class":266},[35,1422,1400],{"class":270},[35,1424,483],{"class":45},[35,1426,486],{"class":270},[35,1428,1392],{"class":45},[35,1430,1431],{"class":270},"].shift()\n",[35,1433,1434,1437,1439,1442,1444,1446,1449,1452,1454],{"class":37,"line":341},[35,1435,1436],{"class":270},"backdated ",[35,1438,293],{"class":266},[35,1440,1441],{"class":270}," seq[seq[",[35,1443,314],{"class":45},[35,1445,1395],{"class":270},[35,1447,1448],{"class":266},"\u003C",[35,1450,1451],{"class":270}," seq[",[35,1453,1416],{"class":45},[35,1455,1456],{"class":270},"]]\n",[35,1458,1459,1461,1464,1466,1468,1470,1472,1474,1476,1478,1480,1482],{"class":37,"line":395},[35,1460,477],{"class":334},[35,1462,1463],{"class":270},"(backdated[[",[35,1465,328],{"class":45},[35,1467,302],{"class":270},[35,1469,314],{"class":45},[35,1471,302],{"class":270},[35,1473,1416],{"class":45},[35,1475,1290],{"class":270},[35,1477,808],{"class":305},[35,1479,293],{"class":266},[35,1481,813],{"class":334},[35,1483,1299],{"class":270},[10,1485,1486],{},"Comparing with the running maximum rather than only the previous row matters when backdated documents come in a run. If invoices 301 and 302 had both been dated in March, a previous-row comparison would flag 301, then compare 302 with 301's March date and let it pass. The running maximum still holds June, so both are reported. Same-day issues are fine, as invoices 299 and 300 show; the comparison is strictly earlier.",[72,1488,81,1494,81,1497,81,1500,81,1505,81,1508,81,1511,81,1514,81,1518,81,1522,81,1526,81,1530,81,1534,81,1537,81,1541,81,1543,81,1546,81,1548,81,1551,81,1553,81,1556,81,1559,81,1563,81,1565,81,1567,81,1569,81,1571,81,1573,81,1575,81,1577,81,1581,81,1583,81,1586,81,1588,81,1590,81,1592,81,1595,81,1598,81,1602,81,1604,81,1607,81,1609,81,1611,81,1613],{"viewBox":1489,"role":75,"ariaLabelledBy":1490,"xmlns":79,"style":1493},"185 0 390 172",[1491,1492],"sq-backdated-t","sq-backdated-d","width:100%;max-width:390px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[83,1495,1496],{"id":1491},"A backdated invoice in the sequence",[87,1498,1499],{"id":1492},"Four consecutive invoice numbers with their issue dates and the latest date seen so far. Invoice 301 is dated 2 March while invoice 300 was dated 22 June, so 301 is earlier than the running maximum and is flagged. Invoice 302 is dated after 22 June and passes.",[91,1501],{"x":1502,"y":94,"width":1503,"height":1504,"fill":97},"185","390","172",[91,1506],{"x":1507,"y":101,"width":1142,"height":1143,"rx":94,"fill":149,"stroke":140,"style":1144},"201.2",[120,1509,455],{"x":1510,"y":1215,"style":1149},"235.9",[91,1512],{"x":1513,"y":101,"width":216,"height":1143,"rx":94,"fill":149,"stroke":140,"style":1144},"270.7",[120,1515,1517],{"x":1516,"y":1215,"style":1149},"318.1","IssueDate",[91,1519],{"x":1520,"y":101,"width":1521,"height":1143,"rx":94,"fill":149,"stroke":140,"style":1144},"365.6","109.3",[120,1523,1525],{"x":1524,"y":1215,"style":1149},"420.3","Latest before it",[91,1527],{"x":1528,"y":101,"width":1529,"height":1143,"rx":94,"fill":149,"stroke":140,"style":1144},"474.9","83.9",[120,1531,1533],{"x":1532,"y":1215,"style":1149},"516.8","Result",[91,1535],{"x":1507,"y":1536,"width":1142,"height":1143,"rx":94,"fill":97,"stroke":106,"style":1153},"44.0",[120,1538,1540],{"x":1510,"y":1539,"style":1157},"63.0","299",[91,1542],{"x":1513,"y":1536,"width":216,"height":1143,"rx":94,"fill":97,"stroke":106,"style":1153},[120,1544,1545],{"x":1516,"y":1539,"style":1157},"2026-06-22",[91,1547],{"x":1520,"y":1536,"width":1521,"height":1143,"rx":94,"fill":97,"stroke":106,"style":1153},[120,1549,1550],{"x":1524,"y":1539,"style":1157},"2026-06-21",[91,1552],{"x":1528,"y":1536,"width":1529,"height":1143,"rx":94,"fill":97,"stroke":106,"style":1153},[120,1554,1555],{"x":1532,"y":1539,"style":1157},"ok",[91,1557],{"x":1507,"y":1558,"width":1142,"height":1143,"rx":94,"fill":97,"stroke":106,"style":1153},"72.0",[120,1560,1562],{"x":1510,"y":1561,"style":1157},"91.0","300",[91,1564],{"x":1513,"y":1558,"width":216,"height":1143,"rx":94,"fill":97,"stroke":106,"style":1153},[120,1566,1545],{"x":1516,"y":1561,"style":1157},[91,1568],{"x":1520,"y":1558,"width":1521,"height":1143,"rx":94,"fill":97,"stroke":106,"style":1153},[120,1570,1545],{"x":1524,"y":1561,"style":1157},[91,1572],{"x":1528,"y":1558,"width":1529,"height":1143,"rx":94,"fill":97,"stroke":106,"style":1153},[120,1574,1555],{"x":1532,"y":1561,"style":1157},[91,1576],{"x":1507,"y":1216,"width":1142,"height":1143,"rx":94,"fill":1191,"stroke":1192,"style":1144},[120,1578,1580],{"x":1510,"y":1579,"style":1195},"119.0","301",[91,1582],{"x":1513,"y":1216,"width":216,"height":1143,"rx":94,"fill":1191,"stroke":1192,"style":1144},[120,1584,1585],{"x":1516,"y":1579,"style":1195},"2026-03-02",[91,1587],{"x":1520,"y":1216,"width":1521,"height":1143,"rx":94,"fill":1191,"stroke":1192,"style":1144},[120,1589,1545],{"x":1524,"y":1579,"style":1195},[91,1591],{"x":1528,"y":1216,"width":1529,"height":1143,"rx":94,"fill":1191,"stroke":1192,"style":1144},[120,1593,1594],{"x":1532,"y":1579,"style":1195},"backdated",[91,1596],{"x":1507,"y":1597,"width":1142,"height":1143,"rx":94,"fill":97,"stroke":106,"style":1153},"128.0",[120,1599,1601],{"x":1510,"y":1600,"style":1157},"147.0","302",[91,1603],{"x":1513,"y":1597,"width":216,"height":1143,"rx":94,"fill":97,"stroke":106,"style":1153},[120,1605,1606],{"x":1516,"y":1600,"style":1157},"2026-06-23",[91,1608],{"x":1520,"y":1597,"width":1521,"height":1143,"rx":94,"fill":97,"stroke":106,"style":1153},[120,1610,1545],{"x":1524,"y":1600,"style":1157},[91,1612],{"x":1528,"y":1597,"width":1529,"height":1143,"rx":94,"fill":97,"stroke":106,"style":1153},[120,1614,1555],{"x":1532,"y":1600,"style":1157},[20,1616,1618],{"id":1617},"step-6-the-exceptions-report","Step 6 — The exceptions report",[10,1620,1621],{},"Put everything in one workbook: a summary per series and a line per exception, with the type of problem, so each can be explained and signed off:",[25,1623,1625],{"className":257,"code":1624,"language":259,"meta":30,"style":30},"exceptions = pd.concat([\n    pd.DataFrame([(s, r, \"Missing\") for s, m in unexplained.items() for r in as_ranges(m)],\n                 columns=[\"Series\", \"Numbers\", \"Issue\"]),\n    dupes.drop_duplicates(\"DocumentNo\").assign(Numbers=lambda d: d[\"Number\"].astype(str), Issue=\"Duplicate\")\n         [[\"Series\", \"Numbers\", \"Issue\"]],\n    backdated.assign(Numbers=lambda d: d[\"Number\"].astype(str),\n                     Issue=lambda d: \"Dated \" + d[\"IssueDate\"].dt.strftime(\"%Y-%m-%d\") + \", before an earlier number\")\n             [[\"Series\", \"Numbers\", \"Issue\"]],\n], ignore_index=True)\nexceptions[\"Explanation\"] = \"\"\n\nsummary = docs.groupby(\"Series\").agg(Documents=(\"Number\", \"size\"), First=(\"Number\", \"min\"), Last=(\"Number\", \"max\"))\nsummary[\"Voided\"] = [len(gaps[s]) - len(unexplained[s]) for s in summary.index]\nsummary[\"Missing\"] = [len(unexplained[s]) for s in summary.index]\nsummary[\"Duplicates\"] = docs[docs.duplicated([\"Series\", \"Number\"])].groupby(\"Series\").size().reindex(summary.index, fill_value=0)\n\nwith pd.ExcelWriter(\"sequence_check.xlsx\", engine=\"xlsxwriter\") as xw:\n    summary.to_excel(xw, sheet_name=\"Summary\")\n    exceptions.to_excel(xw, sheet_name=\"Exceptions\", index=False)\n    xw.sheets[\"Summary\"].set_column(0, 5, 12)\n    xw.sheets[\"Exceptions\"].set_column(0, 1, 12)\n    xw.sheets[\"Exceptions\"].set_column(2, 2, 42)\n    xw.sheets[\"Exceptions\"].set_column(3, 3, 40)\nprint(summary.to_string())\nprint(exceptions.to_string(index=False))\n",[32,1626,1627,1637,1667,1691,1729,1747,1767,1808,1825,1839,1855,1860,1921,1959,1984,2021,2026,2055,2071,2094,2118,2140,2163,2186,2194],{"__ignoreMap":30},[35,1628,1629,1632,1634],{"class":37,"line":38},[35,1630,1631],{"class":270},"exceptions ",[35,1633,293],{"class":266},[35,1635,1636],{"class":270}," pd.concat([\n",[35,1638,1639,1642,1645,1648,1650,1652,1654,1657,1659,1662,1664],{"class":37,"line":280},[35,1640,1641],{"class":270},"    pd.DataFrame([(s, r, ",[35,1643,1644],{"class":45},"\"Missing\"",[35,1646,1647],{"class":270},") ",[35,1649,624],{"class":266},[35,1651,855],{"class":270},[35,1653,630],{"class":266},[35,1655,1656],{"class":270}," unexplained.items() ",[35,1658,624],{"class":266},[35,1660,1661],{"class":270}," r ",[35,1663,630],{"class":266},[35,1665,1666],{"class":270}," as_ranges(m)],\n",[35,1668,1669,1672,1674,1676,1678,1680,1683,1685,1688],{"class":37,"line":287},[35,1670,1671],{"class":305},"                 columns",[35,1673,293],{"class":266},[35,1675,311],{"class":270},[35,1677,483],{"class":45},[35,1679,302],{"class":270},[35,1681,1682],{"class":45},"\"Numbers\"",[35,1684,302],{"class":270},[35,1686,1687],{"class":45},"\"Issue\"",[35,1689,1690],{"class":270},"]),\n",[35,1692,1693,1696,1698,1701,1704,1707,1710,1712,1714,1716,1719,1722,1724,1727],{"class":37,"line":341},[35,1694,1695],{"class":270},"    dupes.drop_duplicates(",[35,1697,328],{"class":45},[35,1699,1700],{"class":270},").assign(",[35,1702,1703],{"class":305},"Numbers",[35,1705,1706],{"class":266},"=lambda",[35,1708,1709],{"class":270}," d: d[",[35,1711,406],{"class":45},[35,1713,465],{"class":270},[35,1715,335],{"class":334},[35,1717,1718],{"class":270},"), ",[35,1720,1721],{"class":305},"Issue",[35,1723,293],{"class":266},[35,1725,1726],{"class":45},"\"Duplicate\"",[35,1728,392],{"class":270},[35,1730,1731,1734,1736,1738,1740,1742,1744],{"class":37,"line":395},[35,1732,1733],{"class":270},"         [[",[35,1735,483],{"class":45},[35,1737,302],{"class":270},[35,1739,1682],{"class":45},[35,1741,302],{"class":270},[35,1743,1687],{"class":45},[35,1745,1746],{"class":270},"]],\n",[35,1748,1749,1752,1754,1756,1758,1760,1762,1764],{"class":37,"line":412},[35,1750,1751],{"class":270},"    backdated.assign(",[35,1753,1703],{"class":305},[35,1755,1706],{"class":266},[35,1757,1709],{"class":270},[35,1759,406],{"class":45},[35,1761,465],{"class":270},[35,1763,335],{"class":334},[35,1765,1766],{"class":270},"),\n",[35,1768,1769,1772,1774,1777,1780,1783,1786,1788,1791,1794,1797,1799,1801,1803,1806],{"class":37,"line":445},[35,1770,1771],{"class":305},"                     Issue",[35,1773,1706],{"class":266},[35,1775,1776],{"class":270}," d: ",[35,1778,1779],{"class":45},"\"Dated \"",[35,1781,1782],{"class":266}," +",[35,1784,1785],{"class":270}," d[",[35,1787,314],{"class":45},[35,1789,1790],{"class":270},"].dt.strftime(",[35,1792,1793],{"class":45},"\"%Y-%m-",[35,1795,1796],{"class":427},"%d",[35,1798,360],{"class":45},[35,1800,1647],{"class":270},[35,1802,384],{"class":266},[35,1804,1805],{"class":45}," \", before an earlier number\"",[35,1807,392],{"class":270},[35,1809,1810,1813,1815,1817,1819,1821,1823],{"class":37,"line":474},[35,1811,1812],{"class":270},"             [[",[35,1814,483],{"class":45},[35,1816,302],{"class":270},[35,1818,1682],{"class":45},[35,1820,302],{"class":270},[35,1822,1687],{"class":45},[35,1824,1746],{"class":270},[35,1826,1827,1829,1832,1834,1837],{"class":37,"line":1082},[35,1828,317],{"class":270},[35,1830,1831],{"class":305},"ignore_index",[35,1833,293],{"class":266},[35,1835,1836],{"class":334},"True",[35,1838,392],{"class":270},[35,1840,1842,1845,1848,1850,1852],{"class":37,"line":1841},10,[35,1843,1844],{"class":270},"exceptions[",[35,1846,1847],{"class":45},"\"Explanation\"",[35,1849,1395],{"class":270},[35,1851,293],{"class":266},[35,1853,1854],{"class":45}," \"\"\n",[35,1856,1858],{"class":37,"line":1857},11,[35,1859,284],{"emptyLinePlaceholder":283},[35,1861,1863,1866,1868,1870,1872,1875,1878,1880,1882,1884,1886,1889,1891,1894,1896,1898,1900,1902,1904,1906,1909,1911,1913,1915,1917,1919],{"class":37,"line":1862},12,[35,1864,1865],{"class":270},"summary ",[35,1867,293],{"class":266},[35,1869,633],{"class":270},[35,1871,483],{"class":45},[35,1873,1874],{"class":270},").agg(",[35,1876,1877],{"class":305},"Documents",[35,1879,293],{"class":266},[35,1881,566],{"class":270},[35,1883,406],{"class":45},[35,1885,302],{"class":270},[35,1887,1888],{"class":45},"\"size\"",[35,1890,1718],{"class":270},[35,1892,1893],{"class":305},"First",[35,1895,293],{"class":266},[35,1897,566],{"class":270},[35,1899,406],{"class":45},[35,1901,302],{"class":270},[35,1903,499],{"class":45},[35,1905,1718],{"class":270},[35,1907,1908],{"class":305},"Last",[35,1910,293],{"class":266},[35,1912,566],{"class":270},[35,1914,406],{"class":45},[35,1916,302],{"class":270},[35,1918,504],{"class":45},[35,1920,1299],{"class":270},[35,1922,1924,1927,1930,1932,1934,1936,1938,1941,1943,1946,1949,1951,1954,1956],{"class":37,"line":1923},13,[35,1925,1926],{"class":270},"summary[",[35,1928,1929],{"class":45},"\"Voided\"",[35,1931,1395],{"class":270},[35,1933,293],{"class":266},[35,1935,995],{"class":270},[35,1937,679],{"class":334},[35,1939,1940],{"class":270},"(gaps[s]) ",[35,1942,599],{"class":266},[35,1944,1945],{"class":334}," len",[35,1947,1948],{"class":270},"(unexplained[s]) ",[35,1950,624],{"class":266},[35,1952,1953],{"class":270}," s ",[35,1955,630],{"class":266},[35,1957,1958],{"class":270}," summary.index]\n",[35,1960,1962,1964,1966,1968,1970,1972,1974,1976,1978,1980,1982],{"class":37,"line":1961},14,[35,1963,1926],{"class":270},[35,1965,1644],{"class":45},[35,1967,1395],{"class":270},[35,1969,293],{"class":266},[35,1971,995],{"class":270},[35,1973,679],{"class":334},[35,1975,1948],{"class":270},[35,1977,624],{"class":266},[35,1979,1953],{"class":270},[35,1981,630],{"class":266},[35,1983,1958],{"class":270},[35,1985,1987,1989,1992,1994,1996,1998,2000,2002,2004,2007,2009,2012,2015,2017,2019],{"class":37,"line":1986},15,[35,1988,1926],{"class":270},[35,1990,1991],{"class":45},"\"Duplicates\"",[35,1993,1395],{"class":270},[35,1995,293],{"class":266},[35,1997,1247],{"class":270},[35,1999,483],{"class":45},[35,2001,302],{"class":270},[35,2003,406],{"class":45},[35,2005,2006],{"class":270},"])].groupby(",[35,2008,483],{"class":45},[35,2010,2011],{"class":270},").size().reindex(summary.index, ",[35,2013,2014],{"class":305},"fill_value",[35,2016,293],{"class":266},[35,2018,94],{"class":334},[35,2020,392],{"class":270},[35,2022,2024],{"class":37,"line":2023},16,[35,2025,284],{"emptyLinePlaceholder":283},[35,2027,2029,2032,2035,2038,2040,2043,2045,2048,2050,2052],{"class":37,"line":2028},17,[35,2030,2031],{"class":266},"with",[35,2033,2034],{"class":270}," pd.ExcelWriter(",[35,2036,2037],{"class":45},"\"sequence_check.xlsx\"",[35,2039,302],{"class":270},[35,2041,2042],{"class":305},"engine",[35,2044,293],{"class":266},[35,2046,2047],{"class":45},"\"xlsxwriter\"",[35,2049,1647],{"class":270},[35,2051,274],{"class":266},[35,2053,2054],{"class":270}," xw:\n",[35,2056,2058,2061,2064,2066,2069],{"class":37,"line":2057},18,[35,2059,2060],{"class":270},"    summary.to_excel(xw, ",[35,2062,2063],{"class":305},"sheet_name",[35,2065,293],{"class":266},[35,2067,2068],{"class":45},"\"Summary\"",[35,2070,392],{"class":270},[35,2072,2074,2077,2079,2081,2084,2086,2088,2090,2092],{"class":37,"line":2073},19,[35,2075,2076],{"class":270},"    exceptions.to_excel(xw, ",[35,2078,2063],{"class":305},[35,2080,293],{"class":266},[35,2082,2083],{"class":45},"\"Exceptions\"",[35,2085,302],{"class":270},[35,2087,808],{"class":305},[35,2089,293],{"class":266},[35,2091,813],{"class":334},[35,2093,392],{"class":270},[35,2095,2097,2100,2102,2105,2107,2109,2112,2114,2116],{"class":37,"line":2096},20,[35,2098,2099],{"class":270},"    xw.sheets[",[35,2101,2068],{"class":45},[35,2103,2104],{"class":270},"].set_column(",[35,2106,94],{"class":334},[35,2108,302],{"class":270},[35,2110,2111],{"class":334},"5",[35,2113,302],{"class":270},[35,2115,104],{"class":334},[35,2117,392],{"class":270},[35,2119,2121,2123,2125,2127,2129,2131,2134,2136,2138],{"class":37,"line":2120},21,[35,2122,2099],{"class":270},[35,2124,2083],{"class":45},[35,2126,2104],{"class":270},[35,2128,94],{"class":334},[35,2130,302],{"class":270},[35,2132,2133],{"class":334},"1",[35,2135,302],{"class":270},[35,2137,104],{"class":334},[35,2139,392],{"class":270},[35,2141,2143,2145,2147,2149,2152,2154,2156,2158,2161],{"class":37,"line":2142},22,[35,2144,2099],{"class":270},[35,2146,2083],{"class":45},[35,2148,2104],{"class":270},[35,2150,2151],{"class":334},"2",[35,2153,302],{"class":270},[35,2155,2151],{"class":334},[35,2157,302],{"class":270},[35,2159,2160],{"class":334},"42",[35,2162,392],{"class":270},[35,2164,2166,2168,2170,2172,2175,2177,2179,2181,2184],{"class":37,"line":2165},23,[35,2167,2099],{"class":270},[35,2169,2083],{"class":45},[35,2171,2104],{"class":270},[35,2173,2174],{"class":334},"3",[35,2176,302],{"class":270},[35,2178,2174],{"class":334},[35,2180,302],{"class":270},[35,2182,2183],{"class":334},"40",[35,2185,392],{"class":270},[35,2187,2189,2191],{"class":37,"line":2188},24,[35,2190,477],{"class":334},[35,2192,2193],{"class":270},"(summary.to_string())\n",[35,2195,2197,2199,2202,2204,2206,2208],{"class":37,"line":2196},25,[35,2198,477],{"class":334},[35,2200,2201],{"class":270},"(exceptions.to_string(",[35,2203,808],{"class":305},[35,2205,293],{"class":266},[35,2207,813],{"class":334},[35,2209,1299],{"class":270},[10,2211,2212],{},"The voided numbers are counted in the summary but not listed as exceptions, since they already have an explanation in the void register. The empty Explanation column is for the person reviewing the rest: \"voided, void register ref 12\", \"test invoice deleted\", \"export error, re-run\". An exceptions report with explanations is what an auditor wants to see.",[20,2214,2216],{"id":2215},"common-pitfalls","Common pitfalls",[2218,2219,2220,2234,2240,2250,2256,2262],"ul",{},[2221,2222,2223,2227,2228,2230,2231,2233],"li",{},[2224,2225,2226],"strong",{},"Numbers read as floats."," Read the ID column as ",[32,2229,335],{},"; ",[32,2232,517],{}," and lost leading zeros break the extraction.",[2221,2235,2236,2239],{},[2224,2237,2238],{},"Mixing series."," Check each prefix separately, or every credit-note number looks like a gap in the invoices.",[2221,2241,2242,2245,2246,2249],{},[2224,2243,2244],{},"Assuming the first number."," ",[32,2247,2248],{},"min()"," finds the first number present; if the series must start at 1, check that explicitly.",[2221,2251,2252,2255],{},[2224,2253,2254],{},"Deleting duplicates."," Two different documents with one number is a finding, not a cleaning step.",[2221,2257,2258,2261],{},[2224,2259,2260],{},"Voided documents."," Many systems keep voided numbers in a separate register; join it in before reporting them as missing.",[2221,2263,2264,2267],{},[2224,2265,2266],{},"Year-end rollover."," A series that restarts each year needs the year in the series key, which the prefix here already contains.",[20,2269,2271],{"id":2270},"conclusion","Conclusion",[10,2273,2274,2275,2278],{},"Read document IDs as text, split them into a series prefix and a trailing number with ",[32,2276,2277],{},"str.extract",", and check each series separately: missing numbers as the difference between the full range and the numbers present, collapsed into ranges; duplicate numbers, distinguishing identical export rows from genuinely different documents; and numbers whose issue date is earlier than the running maximum of lower numbers. Write a summary and an exceptions list with a column for explanations.",[20,2280,2282],{"id":2281},"frequently-asked-questions","Frequently asked questions",[10,2284,2285,2288],{},[2224,2286,2287],{},"How do I find missing numbers in a sequence with pandas?","\nBuild the full range from the minimum to the maximum number and take the set difference with the numbers present. Do it per series with groupby if the column mixes several prefixes.",[10,2290,2291,2294],{},[2224,2292,2293],{},"How do I group consecutive missing numbers into ranges?","\nSort them, subtract each number's position in the list, and group by the result: consecutive numbers share the same value. The minimum and maximum of each group give the range.",[10,2296,2297,2300,2301],{},[2224,2298,2299],{},"How do I extract the number from an ID like INV-2026-000123?","\nUse str.extract with a pattern such as ^(?P",[666,2302,2303,2304],{},".*?)(?P",[2305,2306,2307],"number",{},"\\d+)$, then convert the Number column to int. The Series column keeps the prefix.",[10,2309,2310,2313],{},[2224,2311,2312],{},"Is a gap in invoice numbers always a problem?","\nNot always: voided or cancelled documents leave gaps in many systems. But every gap should be explained, which is why the report lists them with a column for the explanation.",[20,2315,2317],{"id":2316},"related","Related",[2218,2319,2320,2326,2333,2340,2347],{},[2221,2321,2322,2323,2325],{},"Up one level: ",[14,2324,17],{"href":16}," — checks to run before trusting a sheet.",[2221,2327,2328,2332],{},[14,2329,2331],{"href":2330},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fprofile-an-excel-dataset-with-pandas\u002F","Profile an Excel Dataset with pandas"," — a broader first look at a file.",[2221,2334,2335,2339],{},[14,2336,2338],{"href":2337},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Frun-a-benfords-law-check-on-excel-data\u002F","Run a Benford's Law Check on Excel Data"," — another audit test on the same register.",[2221,2341,2342,2346],{},[14,2343,2345],{"href":2344},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffind-overlapping-date-ranges-with-pandas\u002F","Find Overlapping Date Ranges with pandas"," — the date-range version of the same idea.",[2221,2348,2349,2353],{},[14,2350,2352],{"href":2351},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bank-reconciliation-report\u002F","Build a Bank Reconciliation Report"," — matching the register to payments.",[2355,2356,2357],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sx1sP, html code.shiki .sx1sP{--shiki-default:#22863A;--shiki-dark:#72F088}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":30,"searchDepth":280,"depth":280,"links":2359},[2360,2361,2362,2363,2364,2365,2366,2367,2368,2369,2370],{"id":22,"depth":280,"text":23},{"id":250,"depth":280,"text":251},{"id":521,"depth":280,"text":522},{"id":908,"depth":280,"text":909},{"id":1228,"depth":280,"text":1229},{"id":1356,"depth":280,"text":1357},{"id":1617,"depth":280,"text":1618},{"id":2215,"depth":280,"text":2216},{"id":2270,"depth":280,"text":2271},{"id":2281,"depth":280,"text":2282},{"id":2316,"depth":280,"text":2317},"2026-09-29","Check that invoice, cheque or ticket numbers in an Excel export run without gaps: extract the number from IDs such as INV-2026-000123, check each series separately, list missing numbers as compact ranges, catch duplicates and numbers issued out of date order, and write an exceptions report.","md",[2375,2377,2379,2381],{"q":2287,"a":2376},"Build the full range from the minimum to the maximum number and take the set difference with the numbers present. Do it per series with groupby if the column mixes several prefixes.",{"q":2293,"a":2378},"Sort them, subtract each number's position in the list, and group by the result: consecutive numbers share the same value. The minimum and maximum of each group give the range.",{"q":2299,"a":2380},"Use str.extract with a pattern such as ^(?P\u003CSeries>.*?)(?P\u003CNumber>\\d+)$, then convert the Number column to int. The Series column keeps the prefix.",{"q":2312,"a":2382},"Not always: voided or cancelled documents leave gaps in many systems. But every gap should be explained, which is why the report lists them with a column for the explanation.","find gaps in a numbered sequence with pandas",null,{"breadcrumb":2386},[2387,2390,2393],{"name":2388,"item":2389},"Home","\u002F",{"name":2391,"item":2392},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":17,"item":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-gaps-in-a-numbered-sequence-with-pandas",{"title":2396,"description":2397},"Find Missing Numbers and Gaps in a Sequence with pandas","Find gaps in invoice or document numbers from Excel with pandas: extract numeric parts with str.extract, per-series checks with groupby, missing numbers as ranges, duplicates, out-of-order issue dates, and an Excel exceptions report.","find-gaps-in-a-numbered-sequence-with-pandas","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-gaps-in-a-numbered-sequence-with-pandas\u002Findex",[2401,2402],"data-validation","pandas","how-to","8WgEsgBNJxyhhZbHUo5IRVr7nQIgUAASOsVqPCMcaYw",[2406,2410],{"title":2407,"path":2408,"stem":2409,"children":-1},"Find Duplicate Rows in Excel with Python","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-duplicate-rows-in-excel-with-python","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-duplicate-rows-in-excel-with-python\u002Findex",{"title":2411,"path":2412,"stem":2413,"children":-1},"Highlight Invalid Cells in Excel with Python","\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fhighlight-invalid-cells-in-excel-with-python","advanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fhighlight-invalid-cells-in-excel-with-python\u002Findex",[2415,2417],{"path":2416,"title":2391},"\u002Fadvanced-data-transformation-and-cleaning",{"path":2418,"title":17},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python",[],[2421,2424,2427,2430,2433,2436],{"path":2422,"title":2423,"kind":2403},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fdetect-outliers-in-excel-data-with-pandas","Detect Outliers in Excel Data with pandas",{"path":2425,"title":2426,"kind":2403},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl","Highlight Cells Containing Specific Text with openpyxl",{"path":2428,"title":2429,"kind":2403},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Ffind-near-duplicate-names-within-one-excel-column","Find Near-Duplicate Names Within One Excel Column",{"path":2431,"title":2432,"kind":2403},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Fread-dropdown-lists-from-an-existing-workbook","Read Dropdown Lists and Validation Rules from an Existing Workbook",{"path":2434,"title":2435,"kind":2403},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Ffind-and-report-missing-values-in-an-excel-file","Find and Report Missing Values in an Excel File",{"path":2437,"title":2438,"kind":2403},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Fflag-rows-with-too-many-missing-values","Flag Rows with Too Many Missing Values",1790972098675]