[{"data":1,"prerenderedAt":2206},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fextract-numbers-and-codes-from-text-with-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fextract-numbers-and-codes-from-text-with-pandas":2172,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fextract-numbers-and-codes-from-text-with-pandas":2181,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fextract-numbers-and-codes-from-text-with-pandas":2186,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fextract-numbers-and-codes-from-text-with-pandas":2187},{"id":4,"title":5,"body":6,"dateModified":2136,"datePublished":2136,"description":2137,"extension":2138,"faq":2139,"keyword":2148,"kind":2149,"meta":2150,"navigation":351,"path":2160,"seo":2161,"slug":2164,"stem":2165,"topics":2166,"type":2170,"__hash__":2171},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fextract-numbers-and-codes-from-text-with-pandas\u002Findex.md","Extract Numbers and Codes from Text with pandas",{"type":7,"value":8,"toc":2121},"minimark",[9,32,264,269,296,302,306,315,470,496,500,508,618,631,635,653,982,1012,1268,1272,1283,1430,1441,1444,1551,1555,1566,1648,1671,1675,1678,1808,1819,1823,1826,1962,1966,2023,2027,2036,2040,2052,2056,2062,2072,2078,2084,2088,2117],[10,11,12,13,17,18,21,22,25,26,31],"p",{},"Support tickets, bank statement descriptions, order comments and free-text \"reference\" columns hide structured data inside prose: an order number after \"Re:\", a product code in the middle of a sentence, a quantity followed by \"units\", an amount with a currency sign. Excel users pull these out with nested ",[14,15,16],"code",{},"MID",", ",[14,19,20],{},"SEARCH"," and ",[14,23,24],{},"LEN"," formulas that break the first time the wording changes. In pandas, regular expressions do the job in one line per field, and handle variations in spacing, case and punctuation that would defeat the formula approach. This page, part of ",[27,28,30],"a",{"href":29},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002F","Cleaning Excel Data With Pandas",", extracts order numbers, product codes, quantities and amounts from a column of ticket notes.",[33,34,42,43,42,47,42,51,42,59,42,68,42,72,42,79,42,83,42,88,42,90,42,94,42,97,42,101,42,103,42,107,42,110,42,114,42,116,42,119,42,122,42,126,42,128,42,131,42,134,42,138,42,141,42,145,42,149,42,153,42,157,42,159,42,162,42,164,42,167,42,169,42,172,42,174,42,177,42,179,42,181,42,186,42,190,42,192,42,195,42,197,42,199,42,201,42,203,42,205,42,208,42,210,42,213,42,215,42,218,42,220,42,222,42,224,42,226,42,228,42,233,42,237,42,245,42,250,42,258],"svg",{"viewBox":35,"role":36,"ariaLabelledBy":37,"xmlns":40,"style":41},"27 0 705 192","img",[38,39],"rx-extract-t","rx-extract-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:705px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[44,45,46],"title",{"id":38},"Structured fields pulled out of free-text notes",[48,49,50],"desc",{"id":39},"Left: ticket notes such as Re: PO-10442, 3 x SKU AB-1200 @ $12.50, and po 10458 - customer wants 12 units of ab-1203. Right: columns for order number, first product code, quantity and amount, filled where the note contains them: PO-10442, AB-1200, 3, 12.50; PO-10458, AB-1203, 12, blank.",[52,53],"rect",{"x":54,"y":55,"width":56,"height":57,"fill":58},"27","0","705","192","#ffffff",[52,60],{"x":61,"y":62,"width":63,"height":64,"rx":55,"fill":65,"stroke":66,"style":67},"43.9","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[52,69],{"x":70,"y":62,"width":71,"height":64,"rx":55,"fill":65,"stroke":66,"style":67},"75.9","234.4",[73,74,78],"text",{"x":75,"y":76,"style":77},"193.1","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[52,80],{"x":61,"y":81,"width":63,"height":82,"rx":55,"fill":65,"stroke":66,"style":67},"64.0","28.0",[73,84,87],{"x":85,"y":86,"style":77},"59.9","83.0","1",[52,89],{"x":70,"y":81,"width":71,"height":82,"rx":55,"fill":58,"stroke":66,"style":67},[73,91,93],{"x":75,"y":86,"style":92},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","note",[52,95],{"x":61,"y":96,"width":63,"height":82,"rx":55,"fill":65,"stroke":66,"style":67},"92.0",[73,98,100],{"x":85,"y":99,"style":77},"111.0","2",[52,102],{"x":70,"y":96,"width":71,"height":82,"rx":55,"fill":58,"stroke":66,"style":67},[73,104,106],{"x":75,"y":99,"style":105},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Re: PO-10442, 3 x SKU AB-1200…",[52,108],{"x":61,"y":109,"width":63,"height":82,"rx":55,"fill":65,"stroke":66,"style":67},"120.0",[73,111,113],{"x":85,"y":112,"style":77},"139.0","3",[52,115],{"x":70,"y":109,"width":71,"height":82,"rx":55,"fill":58,"stroke":66,"style":67},[73,117,118],{"x":75,"y":112,"style":105},"po 10458 - … 12 units of ab-1203",[52,120],{"x":61,"y":121,"width":63,"height":82,"rx":55,"fill":65,"stroke":66,"style":67},"148.0",[73,123,125],{"x":85,"y":124,"style":77},"167.0","4",[52,127],{"x":70,"y":121,"width":71,"height":82,"rx":55,"fill":58,"stroke":66,"style":67},[73,129,130],{"x":75,"y":124,"style":105},"No order reference given",[52,132],{"x":133,"y":62,"width":63,"height":64,"rx":55,"fill":65,"stroke":66,"style":67},"475.3",[52,135],{"x":136,"y":62,"width":137,"height":64,"rx":55,"fill":65,"stroke":66,"style":67},"507.3","86.7",[73,139,78],{"x":140,"y":76,"style":77},"550.6",[52,142],{"x":143,"y":62,"width":144,"height":64,"rx":55,"fill":65,"stroke":66,"style":67},"594.0","78.0",[73,146,148],{"x":147,"y":76,"style":77},"633.0","B",[52,150],{"x":151,"y":62,"width":152,"height":64,"rx":55,"fill":65,"stroke":66,"style":67},"672.0","44.1",[73,154,156],{"x":155,"y":76,"style":77},"694.0","C",[52,158],{"x":133,"y":81,"width":63,"height":82,"rx":55,"fill":65,"stroke":66,"style":67},[73,160,87],{"x":161,"y":86,"style":77},"491.3",[52,163],{"x":136,"y":81,"width":137,"height":82,"rx":55,"fill":58,"stroke":66,"style":67},[73,165,166],{"x":140,"y":86,"style":92},"order",[52,168],{"x":143,"y":81,"width":144,"height":82,"rx":55,"fill":58,"stroke":66,"style":67},[73,170,171],{"x":147,"y":86,"style":92},"sku",[52,173],{"x":151,"y":81,"width":152,"height":82,"rx":55,"fill":58,"stroke":66,"style":67},[73,175,176],{"x":155,"y":86,"style":92},"qty",[52,178],{"x":133,"y":96,"width":63,"height":82,"rx":55,"fill":65,"stroke":66,"style":67},[73,180,100],{"x":161,"y":99,"style":77},[52,182],{"x":136,"y":96,"width":137,"height":82,"rx":55,"fill":183,"stroke":184,"style":185},"#d9f4f1","var(--teal,#0f9488)","stroke-width:1.5px",[73,187,189],{"x":140,"y":99,"style":188},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","PO-10442",[52,191],{"x":143,"y":96,"width":144,"height":82,"rx":55,"fill":183,"stroke":184,"style":185},[73,193,194],{"x":147,"y":99,"style":188},"AB-1200",[52,196],{"x":151,"y":96,"width":152,"height":82,"rx":55,"fill":58,"stroke":66,"style":67},[73,198,113],{"x":155,"y":99,"style":105},[52,200],{"x":133,"y":109,"width":63,"height":82,"rx":55,"fill":65,"stroke":66,"style":67},[73,202,113],{"x":161,"y":112,"style":77},[52,204],{"x":136,"y":109,"width":137,"height":82,"rx":55,"fill":183,"stroke":184,"style":185},[73,206,207],{"x":140,"y":112,"style":188},"PO-10458",[52,209],{"x":143,"y":109,"width":144,"height":82,"rx":55,"fill":183,"stroke":184,"style":185},[73,211,212],{"x":147,"y":112,"style":188},"AB-1203",[52,214],{"x":151,"y":109,"width":152,"height":82,"rx":55,"fill":58,"stroke":66,"style":67},[73,216,217],{"x":155,"y":112,"style":105},"12",[52,219],{"x":133,"y":121,"width":63,"height":82,"rx":55,"fill":65,"stroke":66,"style":67},[73,221,125],{"x":161,"y":124,"style":77},[52,223],{"x":136,"y":121,"width":137,"height":82,"rx":55,"fill":58,"stroke":66,"style":67},[52,225],{"x":143,"y":121,"width":144,"height":82,"rx":55,"fill":58,"stroke":66,"style":67},[52,227],{"x":151,"y":121,"width":152,"height":82,"rx":55,"fill":58,"stroke":66,"style":67},[73,229,93],{"x":230,"y":231,"style":232},"177.1","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle",[73,234,236],{"x":235,"y":231,"style":232},"595.7","extracted fields",[238,239],"line",{"x1":240,"y1":241,"x2":242,"y2":241,"stroke":243,"style":244},"320.3","108.0","461.3","var(--brand,#5b5cf0)","stroke-width:2px",[246,247],"polygon",{"points":248,"fill":249},"465.3,108.0 456.3,113.0 456.3,103.0","#5b5cf0",[52,251],{"x":252,"y":253,"width":254,"height":255,"rx":256,"fill":58,"stroke":66,"style":257},"342.8","75.0","100.0","22.0","11","stroke-width:1.2px",[73,259,263],{"x":260,"y":261,"style":262},"392.8","90.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","str.extract",[265,266,268],"h2",{"id":267},"prerequisites","Prerequisites",[270,271,276],"pre",{"className":272,"code":273,"language":274,"meta":275,"style":275},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,277,278],{"__ignoreMap":275},[279,280,282,286,290,293],"span",{"class":238,"line":281},1,[279,283,285],{"class":284},"sMTad","pip",[279,287,289],{"class":288},"srMev"," install",[279,291,292],{"class":288}," pandas",[279,294,295],{"class":288}," openpyxl\n",[10,297,298,301],{},[14,299,300],{},"tickets.xlsx"," has six notes: well-formed ones, one in lower case with a space instead of a hyphen, one with two product codes, one with no reference, one with a four-digit order number that looks like a typo, and one with two order numbers and a total.",[265,303,305],{"id":304},"step-1-extract-one-field-with-a-named-group","Step 1 — Extract one field with a named group",[10,307,308,310,311,314],{},[14,309,263],{}," applies a regular expression to every cell and returns the captured groups as columns. Name the group with ",[14,312,313],{},"(?P\u003Cname>...)"," and the column gets that name:",[270,316,320],{"className":317,"code":318,"language":319,"meta":275,"style":275},"language-python shiki shiki-themes github-light github-dark-high-contrast","import re\nimport pandas as pd\n\ntickets = pd.read_excel(\"tickets.xlsx\")\norders = tickets[\"note\"].str.extract(r\"(?P\u003Corder>PO[-\\s]?\\d{5})\", flags=re.IGNORECASE)\nprint(pd.concat([tickets[\"ticket\"], orders], axis=1))\n","python",[14,321,322,332,346,353,371,445],{"__ignoreMap":275},[279,323,324,328],{"class":238,"line":281},[279,325,327],{"class":326},"s-kum","import",[279,329,331],{"class":330},"skGVy"," re\n",[279,333,335,337,340,343],{"class":238,"line":334},2,[279,336,327],{"class":326},[279,338,339],{"class":330}," pandas ",[279,341,342],{"class":326},"as",[279,344,345],{"class":330}," pd\n",[279,347,349],{"class":238,"line":348},3,[279,350,352],{"emptyLinePlaceholder":351},true,"\n",[279,354,356,359,362,365,368],{"class":238,"line":355},4,[279,357,358],{"class":330},"tickets ",[279,360,361],{"class":326},"=",[279,363,364],{"class":330}," pd.read_excel(",[279,366,367],{"class":288},"\"tickets.xlsx\"",[279,369,370],{"class":330},")\n",[279,372,374,377,379,382,385,388,391,394,398,402,405,408,412,415,418,421,424,427,429,431,435,437,440,443],{"class":238,"line":373},5,[279,375,376],{"class":330},"orders ",[279,378,361],{"class":326},[279,380,381],{"class":330}," tickets[",[279,383,384],{"class":288},"\"note\"",[279,386,387],{"class":330},"].str.extract(",[279,389,390],{"class":326},"r",[279,392,393],{"class":288},"\"",[279,395,397],{"class":396},"sP0c6","(",[279,399,401],{"class":400},"sx1sP","?P\u003Corder>",[279,403,404],{"class":288},"PO",[279,406,407],{"class":396},"[",[279,409,411],{"class":410},"sSjpA","-",[279,413,414],{"class":396},"\\s]",[279,416,417],{"class":326},"?",[279,419,420],{"class":396},"\\d",[279,422,423],{"class":326},"{5}",[279,425,426],{"class":396},")",[279,428,393],{"class":288},[279,430,17],{"class":330},[279,432,434],{"class":433},"sa561","flags",[279,436,361],{"class":326},[279,438,439],{"class":330},"re.",[279,441,442],{"class":396},"IGNORECASE",[279,444,370],{"class":330},[279,446,448,451,454,457,460,463,465,467],{"class":238,"line":447},6,[279,449,450],{"class":396},"print",[279,452,453],{"class":330},"(pd.concat([tickets[",[279,455,456],{"class":288},"\"ticket\"",[279,458,459],{"class":330},"], orders], ",[279,461,462],{"class":433},"axis",[279,464,361],{"class":326},[279,466,87],{"class":396},[279,468,469],{"class":330},"))\n",[10,471,472,473,475,476,479,480,483,484,487,488,491,492,495],{},"The pattern allows a hyphen, a space or nothing between ",[14,474,404],{}," and five digits, and ",[14,477,478],{},"re.IGNORECASE"," catches the lower-case ",[14,481,482],{},"po 10458",". Ticket 4, with no reference, gets ",[14,485,486],{},"NaN",". Ticket 5's ",[14,489,490],{},"PO-1047"," has only four digits and is deliberately not matched — a pattern should be as strict as the real format, so that typos surface instead of being accepted. Ticket 6 has two order numbers; ",[14,493,494],{},"extract"," returns only the first.",[265,497,499],{"id":498},"step-2-normalise-what-you-extract","Step 2 — Normalise what you extract",[10,501,502,503,21,505,507],{},"Extracted text inherits the variation of its source: ",[14,504,482],{},[14,506,207],{}," are the same order. Normalise immediately, capturing only the parts that matter and rebuilding a canonical form:",[270,509,511],{"className":317,"code":510,"language":319,"meta":275,"style":275},"parts = tickets[\"note\"].str.extract(r\"PO[-\\s]?(?P\u003Cnum>\\d{5})\", flags=re.IGNORECASE)\ntickets[\"order\"] = \"PO-\" + parts[\"num\"]\nprint(tickets[[\"ticket\", \"order\"]].to_string(index=False))\n",[14,512,513,564,592],{"__ignoreMap":275},[279,514,515,518,520,522,524,526,528,531,533,535,537,539,541,544,546,548,550,552,554,556,558,560,562],{"class":238,"line":281},[279,516,517],{"class":330},"parts ",[279,519,361],{"class":326},[279,521,381],{"class":330},[279,523,384],{"class":288},[279,525,387],{"class":330},[279,527,390],{"class":326},[279,529,530],{"class":288},"\"PO",[279,532,407],{"class":396},[279,534,411],{"class":410},[279,536,414],{"class":396},[279,538,417],{"class":326},[279,540,397],{"class":396},[279,542,543],{"class":400},"?P\u003Cnum>",[279,545,420],{"class":396},[279,547,423],{"class":326},[279,549,426],{"class":396},[279,551,393],{"class":288},[279,553,17],{"class":330},[279,555,434],{"class":433},[279,557,361],{"class":326},[279,559,439],{"class":330},[279,561,442],{"class":396},[279,563,370],{"class":330},[279,565,566,569,572,575,577,580,583,586,589],{"class":238,"line":334},[279,567,568],{"class":330},"tickets[",[279,570,571],{"class":288},"\"order\"",[279,573,574],{"class":330},"] ",[279,576,361],{"class":326},[279,578,579],{"class":288}," \"PO-\"",[279,581,582],{"class":326}," +",[279,584,585],{"class":330}," parts[",[279,587,588],{"class":288},"\"num\"",[279,590,591],{"class":330},"]\n",[279,593,594,596,599,601,603,605,608,611,613,616],{"class":238,"line":348},[279,595,450],{"class":396},[279,597,598],{"class":330},"(tickets[[",[279,600,456],{"class":288},[279,602,17],{"class":330},[279,604,571],{"class":288},[279,606,607],{"class":330},"]].to_string(",[279,609,610],{"class":433},"index",[279,612,361],{"class":326},[279,614,615],{"class":396},"False",[279,617,469],{"class":330},[10,619,620,621,623,624,626,627,630],{},"Capturing just the digits and adding the prefix back means every order number has the same shape, which is what matters when they are later joined against an orders table. Rows without a match stay missing: string concatenation with ",[14,622,486],{}," gives ",[14,625,486],{},", not ",[14,628,629],{},"\"PO-nan\"",".",[265,632,634],{"id":633},"step-3-extract-several-fields-at-once","Step 3 — Extract several fields at once",[10,636,637,638,641,642,645,646,649,650,652],{},"One pattern with several named groups extracts related fields together. Quantities appear as ",[14,639,640],{},"3 x"," or ",[14,643,644],{},"12 units",", and prices as ",[14,647,648],{},"@ $12.50","; making each part optional with ",[14,651,417],{}," keeps a row from failing just because one field is absent:",[270,654,656],{"className":317,"code":655,"language":319,"meta":275,"style":275},"sku = tickets[\"note\"].str.extract(r\"(?P\u003Csku>\\b(?!PO-)[A-Z]{2}-\\d{2,4}\\b)\", flags=re.IGNORECASE)\nqty = tickets[\"note\"].str.extract(r\"(?P\u003Cqty>\\d+)\\s*(?:x\\b|units?\\b)\", flags=re.IGNORECASE)\nprice = tickets[\"note\"].str.extract(r\"@\\s*\\$(?P\u003Cprice>[\\d,]+(?:\\.\\d{2})?)\")\n\nfields = pd.concat([tickets[[\"ticket\", \"order\"]], sku, qty, price], axis=1)\nfields[\"sku\"] = fields[\"sku\"].str.upper()\nfields[\"qty\"] = pd.to_numeric(fields[\"qty\"]).astype(\"Int64\")\nfields[\"price\"] = pd.to_numeric(fields[\"price\"].str.replace(\",\", \"\", regex=False))\nprint(fields.to_string(index=False))\n",[14,657,658,726,791,852,856,883,903,928,966],{"__ignoreMap":275},[279,659,660,663,665,667,669,671,673,675,677,680,683,686,689,691,693,696,699,702,704,706,709,712,714,716,718,720,722,724],{"class":238,"line":281},[279,661,662],{"class":330},"sku ",[279,664,361],{"class":326},[279,666,381],{"class":330},[279,668,384],{"class":288},[279,670,387],{"class":330},[279,672,390],{"class":326},[279,674,393],{"class":288},[279,676,397],{"class":396},[279,678,679],{"class":400},"?P\u003Csku>",[279,681,682],{"class":396},"\\b",[279,684,685],{"class":326},"(?!",[279,687,688],{"class":288},"PO-",[279,690,426],{"class":326},[279,692,407],{"class":396},[279,694,695],{"class":410},"A-Z",[279,697,698],{"class":396},"]",[279,700,701],{"class":326},"{2}",[279,703,411],{"class":288},[279,705,420],{"class":396},[279,707,708],{"class":326},"{2,4}",[279,710,711],{"class":396},"\\b)",[279,713,393],{"class":288},[279,715,17],{"class":330},[279,717,434],{"class":433},[279,719,361],{"class":326},[279,721,439],{"class":330},[279,723,442],{"class":396},[279,725,370],{"class":330},[279,727,728,731,733,735,737,739,741,743,745,748,750,753,756,759,762,765,767,770,773,775,777,779,781,783,785,787,789],{"class":238,"line":334},[279,729,730],{"class":330},"qty ",[279,732,361],{"class":326},[279,734,381],{"class":330},[279,736,384],{"class":288},[279,738,387],{"class":330},[279,740,390],{"class":326},[279,742,393],{"class":288},[279,744,397],{"class":396},[279,746,747],{"class":400},"?P\u003Cqty>",[279,749,420],{"class":396},[279,751,752],{"class":326},"+",[279,754,755],{"class":396},")\\s",[279,757,758],{"class":326},"*",[279,760,761],{"class":396},"(?:",[279,763,764],{"class":288},"x",[279,766,682],{"class":396},[279,768,769],{"class":326},"|",[279,771,772],{"class":288},"units",[279,774,417],{"class":326},[279,776,711],{"class":396},[279,778,393],{"class":288},[279,780,17],{"class":330},[279,782,434],{"class":433},[279,784,361],{"class":326},[279,786,439],{"class":330},[279,788,442],{"class":396},[279,790,370],{"class":330},[279,792,793,796,798,800,802,804,806,809,812,814,818,820,823,826,829,831,833,835,838,840,842,844,846,848,850],{"class":238,"line":348},[279,794,795],{"class":330},"price ",[279,797,361],{"class":326},[279,799,381],{"class":330},[279,801,384],{"class":288},[279,803,387],{"class":330},[279,805,390],{"class":326},[279,807,808],{"class":288},"\"@",[279,810,811],{"class":396},"\\s",[279,813,758],{"class":326},[279,815,817],{"class":816},"s_b0D","\\$",[279,819,397],{"class":396},[279,821,822],{"class":400},"?P\u003Cprice>",[279,824,825],{"class":396},"[\\d",[279,827,828],{"class":410},",",[279,830,698],{"class":396},[279,832,752],{"class":326},[279,834,761],{"class":396},[279,836,837],{"class":816},"\\.",[279,839,420],{"class":396},[279,841,701],{"class":326},[279,843,426],{"class":396},[279,845,417],{"class":326},[279,847,426],{"class":396},[279,849,393],{"class":288},[279,851,370],{"class":330},[279,853,854],{"class":238,"line":355},[279,855,352],{"emptyLinePlaceholder":351},[279,857,858,861,863,866,868,870,872,875,877,879,881],{"class":238,"line":373},[279,859,860],{"class":330},"fields ",[279,862,361],{"class":326},[279,864,865],{"class":330}," pd.concat([tickets[[",[279,867,456],{"class":288},[279,869,17],{"class":330},[279,871,571],{"class":288},[279,873,874],{"class":330},"]], sku, qty, price], ",[279,876,462],{"class":433},[279,878,361],{"class":326},[279,880,87],{"class":396},[279,882,370],{"class":330},[279,884,885,888,891,893,895,898,900],{"class":238,"line":447},[279,886,887],{"class":330},"fields[",[279,889,890],{"class":288},"\"sku\"",[279,892,574],{"class":330},[279,894,361],{"class":326},[279,896,897],{"class":330}," fields[",[279,899,890],{"class":288},[279,901,902],{"class":330},"].str.upper()\n",[279,904,906,908,911,913,915,918,920,923,926],{"class":238,"line":905},7,[279,907,887],{"class":330},[279,909,910],{"class":288},"\"qty\"",[279,912,574],{"class":330},[279,914,361],{"class":326},[279,916,917],{"class":330}," pd.to_numeric(fields[",[279,919,910],{"class":288},[279,921,922],{"class":330},"]).astype(",[279,924,925],{"class":288},"\"Int64\"",[279,927,370],{"class":330},[279,929,931,933,936,938,940,942,944,947,950,952,955,957,960,962,964],{"class":238,"line":930},8,[279,932,887],{"class":330},[279,934,935],{"class":288},"\"price\"",[279,937,574],{"class":330},[279,939,361],{"class":326},[279,941,917],{"class":330},[279,943,935],{"class":288},[279,945,946],{"class":330},"].str.replace(",[279,948,949],{"class":288},"\",\"",[279,951,17],{"class":330},[279,953,954],{"class":288},"\"\"",[279,956,17],{"class":330},[279,958,959],{"class":433},"regex",[279,961,361],{"class":326},[279,963,615],{"class":396},[279,965,469],{"class":330},[279,967,969,971,974,976,978,980],{"class":238,"line":968},9,[279,970,450],{"class":396},[279,972,973],{"class":330},"(fields.to_string(",[279,975,610],{"class":433},[279,977,361],{"class":326},[279,979,615],{"class":396},[279,981,469],{"class":330},[10,983,984,985,987,988,991,992,994,995,998,999,1002,1003,1007,1008,1011],{},"Separate patterns per field are easier to read and debug than one enormous expression, and each can be tested on its own. They can also collide: product codes are two letters, a hyphen and two to four digits, and so is the malformed order number ",[14,986,490],{}," in ticket 5. The first version of this pattern reported it as a product. The negative lookahead ",[14,989,990],{},"(?!PO-)"," excludes anything starting with the order prefix — a reminder to run every pattern against the other fields' values, not just its own. The word boundaries (",[14,993,682],{},") in the SKU pattern stop it matching the middle of a longer token. Converting types right after extracting — ",[14,996,997],{},"Int64"," for quantities, numbers for prices — turns the text into data you can sum. Notice that ticket 5 writes its quantity as ",[14,1000,1001],{},"qty 5",", with the number ",[1004,1005,1006],"em",{},"after"," the word, which the pattern does not cover. Whether to add that form as an alternative — ",[14,1009,1010],{},"(?:qty\\s*(?P\u003Cqty2>\\d+))"," — depends on how often a source uses it; the point is that the gap is visible in the output rather than hidden.",[33,1013,42,1019,42,1022,42,1025,42,1029,42,1035,42,1040,42,1049,42,1052,42,1083,42,1086,42,1090,42,1092,42,1104,42,1107,42,1111,42,1113,42,1156,42,1159,42,1162,42,1164,42,1201,42,1208,42,1211,42,1214,42,1219,42,1223,42,1226,42,1229,42,1231,42,1235,42,1238,42,1243,42,1246,42,1248,42,1252,42,1255,42,1259,42,1262,42,1265],{"viewBox":1014,"role":36,"ariaLabelledBy":1015,"xmlns":40,"style":1018},"4 0 752 260",[1016,1017],"rx-anatomy-t","rx-anatomy-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[44,1020,1021],{"id":1016},"Anatomy of the SKU pattern",[48,1023,1024],{"id":1017},"The regular expression for product codes, with callouts. The named group gives the output column its name. The word boundaries stop matches inside longer tokens. Two letters, a hyphen and two to four digits describe the real code format. The IGNORECASE flag accepts lower-case input, which is normalised afterwards with str.upper.",[52,1026],{"x":125,"y":55,"width":1027,"height":1028,"fill":58},"752","260",[52,1030],{"x":1031,"y":1032,"width":1033,"height":241,"rx":217,"fill":1034,"stroke":66,"style":185},"20.0","16.0","720.0","#f0f2f5",[73,1036,87],{"x":1037,"y":1038,"style":1039},"60.0","47.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",[1041,1042],"circle",{"cx":1043,"cy":1044,"r":1045,"fill":1046,"stroke":1047,"style":1048},"36.0","42.5","11.0","#4338ca","none","stroke-width:0px",[73,1050,87],{"x":1043,"y":1038,"style":1051},"font-size:12px;font-weight:700;fill:#ffffff;text-anchor:middle",[73,1053,1056,1061,1064,1066,1068,1070,1072,1076,1079],{"x":1054,"y":1038,"style":1055},"82.0","font-size:12.5px;text-anchor:start;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",[1057,1058,1060],"tspan",{"style":1059},"fill:var(--text,#172033);font-weight:400","pattern",[1057,1062,1063],{"style":1059}," ",[1057,1065,361],{"style":1059},[1057,1067,1063],{"style":1059},[1057,1069,397],{"style":1059},[1057,1071,390],{"style":1059},[1057,1073,1075],{"style":1074},"fill:var(--teal-ink,#0b6157);font-weight:400","\"(?P\u003Csku>\"",[1057,1077,1078],{"style":1059},"            ",[1057,1080,1082],{"style":1081},"fill:var(--muted,#5b6780);font-weight:400","# named group -> column \"sku\"",[73,1084,100],{"x":1037,"y":1085,"style":1039},"68.0",[1041,1087],{"cx":1043,"cy":1088,"r":1045,"fill":1089,"stroke":1047,"style":1048},"63.5","#0b6157",[73,1091,100],{"x":1043,"y":1085,"style":1051},[73,1093,1094,1097,1099,1102],{"x":1054,"y":1085,"style":1055},[1057,1095,1096],{"style":1059},"           ",[1057,1098,390],{"style":1059},[1057,1100,1101],{"style":1074},"\"\\b(?!PO-)[A-Z]{2}-\\d{2,4}\\b)\"",[1057,1103,426],{"style":1059},[73,1105,113],{"x":1037,"y":1106,"style":1039},"89.0",[1041,1108],{"cx":1043,"cy":1109,"r":1045,"fill":1110,"stroke":1047,"style":1048},"84.5","#8a5808",[73,1112,113],{"x":1043,"y":1106,"style":1051},[73,1114,1115,1117,1119,1121,1123,1126,1128,1131,1133,1135,1137,1139,1141,1143,1145,1147,1150,1152,1154],{"x":1054,"y":1106,"style":1055},[1057,1116,171],{"style":1059},[1057,1118,1063],{"style":1059},[1057,1120,361],{"style":1059},[1057,1122,1063],{"style":1059},[1057,1124,1125],{"style":1059},"notes",[1057,1127,630],{"style":1059},[1057,1129,1130],{"style":1059},"str",[1057,1132,630],{"style":1059},[1057,1134,494],{"style":1059},[1057,1136,397],{"style":1059},[1057,1138,1060],{"style":1059},[1057,1140,828],{"style":1059},[1057,1142,1063],{"style":1059},[1057,1144,434],{"style":1059},[1057,1146,361],{"style":1059},[1057,1148,1149],{"style":1059},"re",[1057,1151,630],{"style":1059},[1057,1153,442],{"style":1059},[1057,1155,426],{"style":1059},[73,1157,125],{"x":1037,"y":1158,"style":1039},"110.0",[1041,1160],{"cx":1043,"cy":1161,"r":1045,"fill":1089,"stroke":1047,"style":1048},"105.5",[73,1163,125],{"x":1043,"y":1158,"style":1051},[73,1165,1166,1168,1170,1172,1174,1176,1178,1180,1182,1184,1186,1188,1190,1192,1194,1197,1199],{"x":1054,"y":1158,"style":1055},[1057,1167,171],{"style":1059},[1057,1169,407],{"style":1059},[1057,1171,890],{"style":1074},[1057,1173,698],{"style":1059},[1057,1175,1063],{"style":1059},[1057,1177,361],{"style":1059},[1057,1179,1063],{"style":1059},[1057,1181,171],{"style":1059},[1057,1183,407],{"style":1059},[1057,1185,890],{"style":1074},[1057,1187,698],{"style":1059},[1057,1189,630],{"style":1059},[1057,1191,1130],{"style":1059},[1057,1193,630],{"style":1059},[1057,1195,1196],{"style":1059},"upper",[1057,1198,397],{"style":1059},[1057,1200,426],{"style":1059},[52,1202],{"x":1031,"y":1203,"width":1204,"height":1205,"rx":1206,"fill":1207,"stroke":243,"style":185},"138.0","230.7","52.0","10","#f0f4ff",[1041,1209],{"cx":62,"cy":1210,"r":1045,"fill":1046,"stroke":1047,"style":1048},"164.0",[73,1212,87],{"x":62,"y":1213,"style":1051},"168.5",[73,1215,1218],{"x":1037,"y":1216,"style":1217},"160.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","(?P\u003Csku>…) names the",[73,1220,1222],{"x":1037,"y":1221,"style":1217},"177.0","output column",[52,1224],{"x":1225,"y":1203,"width":1204,"height":1205,"rx":1206,"fill":183,"stroke":184,"style":185},"264.7",[1041,1227],{"cx":1228,"cy":1210,"r":1045,"fill":1089,"stroke":1047,"style":1048},"284.7",[73,1230,100],{"x":1228,"y":1213,"style":1051},[73,1232,1234],{"x":1233,"y":1216,"style":1217},"304.7","\\b … \\b whole tokens; (?!PO-)",[73,1236,1237],{"x":1233,"y":1221,"style":1217},"skips orders",[52,1239],{"x":1240,"y":1203,"width":1204,"height":1205,"rx":1206,"fill":1241,"stroke":1242,"style":185},"509.3","#fdefd8","var(--gold,#b4740a)",[1041,1244],{"cx":1245,"cy":1210,"r":1045,"fill":1110,"stroke":1047,"style":1048},"529.3",[73,1247,113],{"x":1245,"y":1213,"style":1051},[73,1249,1251],{"x":1250,"y":1216,"style":1217},"549.3","IGNORECASE: accept",[73,1253,1254],{"x":1250,"y":1221,"style":1217},"ab-1203",[52,1256],{"x":1031,"y":1257,"width":1204,"height":1258,"rx":1206,"fill":183,"stroke":184,"style":185},"200.0","44.0",[1041,1260],{"cx":62,"cy":1261,"r":1045,"fill":1089,"stroke":1047,"style":1048},"222.0",[73,1263,125],{"x":62,"y":1264,"style":1051},"226.5",[73,1266,1267],{"x":1037,"y":1264,"style":1217},"normalise to AB-1203",[265,1269,1271],{"id":1270},"step-4-extract-every-match-with-extractall","Step 4 — Extract every match with extractall",[10,1273,1274,1275,1278,1279,1282],{},"Ticket 3 mentions two products and ticket 6 two orders. ",[14,1276,1277],{},"str.extractall"," returns every match, with an extra index level, ",[14,1280,1281],{},"match",", counting the matches within each cell:",[270,1284,1286],{"className":317,"code":1285,"language":319,"meta":275,"style":275},"all_skus = tickets[\"note\"].str.extractall(r\"(?P\u003Csku>\\b(?!PO-)[A-Z]{2}-\\d{2,4}\\b)\", flags=re.IGNORECASE)\nall_skus[\"sku\"] = all_skus[\"sku\"].str.upper()\nprint(all_skus)\n\nlines = all_skus.reset_index(level=\"match\").join(tickets[\"ticket\"])\nprint(lines[[\"ticket\", \"match\", \"sku\"]].to_string(index=False))\n",[14,1287,1288,1348,1366,1373,1377,1403],{"__ignoreMap":275},[279,1289,1290,1293,1295,1297,1299,1302,1304,1306,1308,1310,1312,1314,1316,1318,1320,1322,1324,1326,1328,1330,1332,1334,1336,1338,1340,1342,1344,1346],{"class":238,"line":281},[279,1291,1292],{"class":330},"all_skus ",[279,1294,361],{"class":326},[279,1296,381],{"class":330},[279,1298,384],{"class":288},[279,1300,1301],{"class":330},"].str.extractall(",[279,1303,390],{"class":326},[279,1305,393],{"class":288},[279,1307,397],{"class":396},[279,1309,679],{"class":400},[279,1311,682],{"class":396},[279,1313,685],{"class":326},[279,1315,688],{"class":288},[279,1317,426],{"class":326},[279,1319,407],{"class":396},[279,1321,695],{"class":410},[279,1323,698],{"class":396},[279,1325,701],{"class":326},[279,1327,411],{"class":288},[279,1329,420],{"class":396},[279,1331,708],{"class":326},[279,1333,711],{"class":396},[279,1335,393],{"class":288},[279,1337,17],{"class":330},[279,1339,434],{"class":433},[279,1341,361],{"class":326},[279,1343,439],{"class":330},[279,1345,442],{"class":396},[279,1347,370],{"class":330},[279,1349,1350,1353,1355,1357,1359,1362,1364],{"class":238,"line":334},[279,1351,1352],{"class":330},"all_skus[",[279,1354,890],{"class":288},[279,1356,574],{"class":330},[279,1358,361],{"class":326},[279,1360,1361],{"class":330}," all_skus[",[279,1363,890],{"class":288},[279,1365,902],{"class":330},[279,1367,1368,1370],{"class":238,"line":348},[279,1369,450],{"class":396},[279,1371,1372],{"class":330},"(all_skus)\n",[279,1374,1375],{"class":238,"line":355},[279,1376,352],{"emptyLinePlaceholder":351},[279,1378,1379,1382,1384,1387,1390,1392,1395,1398,1400],{"class":238,"line":373},[279,1380,1381],{"class":330},"lines ",[279,1383,361],{"class":326},[279,1385,1386],{"class":330}," all_skus.reset_index(",[279,1388,1389],{"class":433},"level",[279,1391,361],{"class":326},[279,1393,1394],{"class":288},"\"match\"",[279,1396,1397],{"class":330},").join(tickets[",[279,1399,456],{"class":288},[279,1401,1402],{"class":330},"])\n",[279,1404,1405,1407,1410,1412,1414,1416,1418,1420,1422,1424,1426,1428],{"class":238,"line":447},[279,1406,450],{"class":396},[279,1408,1409],{"class":330},"(lines[[",[279,1411,456],{"class":288},[279,1413,17],{"class":330},[279,1415,1394],{"class":288},[279,1417,17],{"class":330},[279,1419,890],{"class":288},[279,1421,607],{"class":330},[279,1423,610],{"class":433},[279,1425,361],{"class":326},[279,1427,615],{"class":396},[279,1429,469],{"class":330},[10,1431,1432,1433,1436,1437,1440],{},"The result has one row per match, which is the right shape for a \"ticket lines\" table: ticket 3 contributes two rows, tickets without a code contribute none. Joining back on the original index attaches the ticket number. When you only need the matches as a list per cell, ",[14,1434,1435],{},"str.findall"," returns Python lists instead, which suits a quick ",[14,1438,1439],{},"explode"," or a count of matches per row.",[10,1442,1443],{},"Which of the three methods to reach for depends on how many values a cell can hold and what shape you want back:",[33,1445,42,1450,42,1453,42,1456,42,1459,42,1462,42,1466,42,1472,42,1475,42,1479,42,1482,42,1486,42,1490,42,1493,42,1495,42,1500,42,1502,42,1505,42,1507,42,1510,42,1512,42,1515,42,1517,42,1520,42,1522,42,1525,42,1527,42,1530,42,1533,42,1536,42,1538,42,1541,42,1543,42,1546,42,1548],{"viewBox":1446,"role":36,"ariaLabelledBy":1447,"xmlns":40,"style":1018},"4 0 752 174",[1448,1449],"rx-methods-t","rx-methods-d",[44,1451,1452],{"id":1448},"extract, extractall and findall compared",[48,1454,1455],{"id":1449},"A comparison of three pandas string methods. str.extract returns the first match per cell as columns, one output row per input row. str.extractall returns every match as rows with an extra match index. str.findall returns every match as a Python list per cell. All three take flags such as IGNORECASE.",[52,1457],{"x":125,"y":55,"width":1027,"height":1458,"fill":58},"174",[52,1460],{"x":1031,"y":1032,"width":1461,"height":62,"rx":55,"fill":58,"stroke":66,"style":67},"110.3",[52,1463],{"x":1464,"y":1032,"width":1465,"height":62,"rx":55,"fill":1207,"stroke":243,"style":257},"130.3","203.2",[73,1467,1471],{"x":1468,"y":1469,"style":1470},"232.0","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Matches per cell",[52,1473],{"x":1474,"y":1032,"width":1465,"height":62,"rx":55,"fill":1207,"stroke":243,"style":257},"333.6",[73,1476,1478],{"x":1477,"y":1469,"style":1470},"435.2","Returns",[52,1480],{"x":1481,"y":1032,"width":1465,"height":62,"rx":55,"fill":1207,"stroke":243,"style":257},"536.8",[73,1483,1485],{"x":1484,"y":1469,"style":1470},"638.4","Best for",[52,1487],{"x":1031,"y":1488,"width":1461,"height":1489,"rx":55,"fill":65,"stroke":66,"style":67},"56.0","34.0",[73,1491,263],{"x":63,"y":144,"style":1492},"font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start",[52,1494],{"x":1464,"y":1488,"width":1465,"height":1489,"rx":55,"fill":58,"stroke":66,"style":67},[73,1496,1499],{"x":1468,"y":1497,"style":1498},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","first only",[52,1501],{"x":1474,"y":1488,"width":1465,"height":1489,"rx":55,"fill":58,"stroke":66,"style":67},[73,1503,1504],{"x":1477,"y":1497,"style":1498},"DataFrame, one row per cell",[52,1506],{"x":1481,"y":1488,"width":1465,"height":1489,"rx":55,"fill":58,"stroke":66,"style":67},[73,1508,1509],{"x":1484,"y":1497,"style":1498},"one field per record",[52,1511],{"x":1031,"y":261,"width":1461,"height":1489,"rx":55,"fill":65,"stroke":66,"style":67},[73,1513,1277],{"x":63,"y":1514,"style":1492},"112.0",[52,1516],{"x":1464,"y":261,"width":1465,"height":1489,"rx":55,"fill":58,"stroke":66,"style":67},[73,1518,1519],{"x":1468,"y":99,"style":1498},"all",[52,1521],{"x":1474,"y":261,"width":1465,"height":1489,"rx":55,"fill":58,"stroke":66,"style":67},[73,1523,1524],{"x":1477,"y":99,"style":1498},"rows with a match index",[52,1526],{"x":1481,"y":261,"width":1465,"height":1489,"rx":55,"fill":58,"stroke":66,"style":67},[73,1528,1529],{"x":1484,"y":99,"style":1498},"line tables (ticket → SKUs)",[52,1531],{"x":1031,"y":1532,"width":1461,"height":1489,"rx":55,"fill":65,"stroke":66,"style":67},"124.0",[73,1534,1435],{"x":63,"y":1535,"style":1492},"146.0",[52,1537],{"x":1464,"y":1532,"width":1465,"height":1489,"rx":55,"fill":58,"stroke":66,"style":67},[73,1539,1519],{"x":1468,"y":1540,"style":1498},"145.0",[52,1542],{"x":1474,"y":1532,"width":1465,"height":1489,"rx":55,"fill":58,"stroke":66,"style":67},[73,1544,1545],{"x":1477,"y":1540,"style":1498},"a list per cell",[52,1547],{"x":1481,"y":1532,"width":1465,"height":1489,"rx":55,"fill":58,"stroke":66,"style":67},[73,1549,1550],{"x":1484,"y":1540,"style":1498},"counts and quick checks",[265,1552,1554],{"id":1553},"step-5-validate-codes-that-are-supposed-to-be-codes","Step 5 — Validate codes that are supposed to be codes",[10,1556,1557,1558,1561,1562,1565],{},"Extraction finds codes inside text. When a column should ",[1004,1559,1560],{},"be"," a code — a reference column that sometimes holds rubbish — check it with ",[14,1563,1564],{},"str.fullmatch",", which requires the whole value to match:",[270,1567,1569],{"className":317,"code":1568,"language":319,"meta":275,"style":275},"refs = pd.Series([\"PO-10442\", \"PO-1047\", \"po-10458\", \"PO-10460 \", \"see email\"])\nvalid = refs.str.strip().str.upper().str.fullmatch(r\"PO-\\d{5}\")\nprint(pd.DataFrame({\"ref\": refs, \"valid\": valid}))\n",[14,1570,1571,1606,1629],{"__ignoreMap":275},[279,1572,1573,1576,1578,1581,1584,1586,1589,1591,1594,1596,1599,1601,1604],{"class":238,"line":281},[279,1574,1575],{"class":330},"refs ",[279,1577,361],{"class":326},[279,1579,1580],{"class":330}," pd.Series([",[279,1582,1583],{"class":288},"\"PO-10442\"",[279,1585,17],{"class":330},[279,1587,1588],{"class":288},"\"PO-1047\"",[279,1590,17],{"class":330},[279,1592,1593],{"class":288},"\"po-10458\"",[279,1595,17],{"class":330},[279,1597,1598],{"class":288},"\"PO-10460 \"",[279,1600,17],{"class":330},[279,1602,1603],{"class":288},"\"see email\"",[279,1605,1402],{"class":330},[279,1607,1608,1611,1613,1616,1618,1621,1623,1625,1627],{"class":238,"line":334},[279,1609,1610],{"class":330},"valid ",[279,1612,361],{"class":326},[279,1614,1615],{"class":330}," refs.str.strip().str.upper().str.fullmatch(",[279,1617,390],{"class":326},[279,1619,1620],{"class":288},"\"PO-",[279,1622,420],{"class":396},[279,1624,423],{"class":326},[279,1626,393],{"class":288},[279,1628,370],{"class":330},[279,1630,1631,1633,1636,1639,1642,1645],{"class":238,"line":348},[279,1632,450],{"class":396},[279,1634,1635],{"class":330},"(pd.DataFrame({",[279,1637,1638],{"class":288},"\"ref\"",[279,1640,1641],{"class":330},": refs, ",[279,1643,1644],{"class":288},"\"valid\"",[279,1646,1647],{"class":330},": valid}))\n",[10,1649,1650,1653,1654,641,1657,1659,1660,1662,1663,1666,1667,630],{},[14,1651,1652],{},"fullmatch"," is stricter than ",[14,1655,1656],{},"contains",[14,1658,494],{},": ",[14,1661,490],{}," fails for having four digits and ",[14,1664,1665],{},"see email"," fails outright, while stripping and upper-casing first lets harmless variations through. Use it to separate clean values from the ones that need a person, as in ",[27,1668,1670],{"href":1669},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-columns-before-import-with-pandas\u002F","Validate Excel Columns Before Import with pandas",[265,1672,1674],{"id":1673},"step-6-report-the-rows-where-nothing-matched","Step 6 — Report the rows where nothing matched",[10,1676,1677],{},"Every extraction should end with a look at what it missed. Rows without a match are either genuinely missing the field or evidence that the pattern is wrong:",[270,1679,1681],{"className":317,"code":1680,"language":319,"meta":275,"style":275},"missing = fields[fields[\"order\"].isna()].merge(tickets[[\"ticket\", \"note\"]], on=\"ticket\")\nprint(missing[[\"ticket\", \"note\"]].to_string(index=False))\n\nsuspicious = tickets[\"note\"].str.contains(r\"\\bPO\\b\", case=False) & fields[\"order\"].isna()\nprint(\"mentions PO but no valid order:\", tickets.loc[suspicious, \"ticket\"].tolist())\n",[14,1682,1683,1716,1739,1743,1791],{"__ignoreMap":275},[279,1684,1685,1688,1690,1693,1695,1698,1700,1702,1704,1707,1710,1712,1714],{"class":238,"line":281},[279,1686,1687],{"class":330},"missing ",[279,1689,361],{"class":326},[279,1691,1692],{"class":330}," fields[fields[",[279,1694,571],{"class":288},[279,1696,1697],{"class":330},"].isna()].merge(tickets[[",[279,1699,456],{"class":288},[279,1701,17],{"class":330},[279,1703,384],{"class":288},[279,1705,1706],{"class":330},"]], ",[279,1708,1709],{"class":433},"on",[279,1711,361],{"class":326},[279,1713,456],{"class":288},[279,1715,370],{"class":330},[279,1717,1718,1720,1723,1725,1727,1729,1731,1733,1735,1737],{"class":238,"line":334},[279,1719,450],{"class":396},[279,1721,1722],{"class":330},"(missing[[",[279,1724,456],{"class":288},[279,1726,17],{"class":330},[279,1728,384],{"class":288},[279,1730,607],{"class":330},[279,1732,610],{"class":433},[279,1734,361],{"class":326},[279,1736,615],{"class":396},[279,1738,469],{"class":330},[279,1740,1741],{"class":238,"line":348},[279,1742,352],{"emptyLinePlaceholder":351},[279,1744,1745,1748,1750,1752,1754,1757,1759,1761,1763,1765,1767,1769,1771,1774,1776,1778,1781,1784,1786,1788],{"class":238,"line":355},[279,1746,1747],{"class":330},"suspicious ",[279,1749,361],{"class":326},[279,1751,381],{"class":330},[279,1753,384],{"class":288},[279,1755,1756],{"class":330},"].str.contains(",[279,1758,390],{"class":326},[279,1760,393],{"class":288},[279,1762,682],{"class":396},[279,1764,404],{"class":288},[279,1766,682],{"class":396},[279,1768,393],{"class":288},[279,1770,17],{"class":330},[279,1772,1773],{"class":433},"case",[279,1775,361],{"class":326},[279,1777,615],{"class":396},[279,1779,1780],{"class":330},") ",[279,1782,1783],{"class":326},"&",[279,1785,897],{"class":330},[279,1787,571],{"class":288},[279,1789,1790],{"class":330},"].isna()\n",[279,1792,1793,1795,1797,1800,1803,1805],{"class":238,"line":373},[279,1794,450],{"class":396},[279,1796,397],{"class":330},[279,1798,1799],{"class":288},"\"mentions PO but no valid order:\"",[279,1801,1802],{"class":330},", tickets.loc[suspicious, ",[279,1804,456],{"class":288},[279,1806,1807],{"class":330},"].tolist())\n",[10,1809,1810,1811,1813,1814,1818],{},"Ticket 4 genuinely has no reference. Ticket 5 mentions ",[14,1812,404],{}," but its number is malformed, which is exactly the case a person should see — the second check catches it by looking for the prefix without the full pattern. Writing both lists to a \"Needs review\" sheet next to the cleaned data closes the loop. ",[27,1815,1817],{"href":1816},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl\u002F","Highlight Cells Containing Specific Text with openpyxl"," shows how to mark such rows in the workbook itself.",[265,1820,1822],{"id":1821},"writing-the-results-to-excel","Writing the results to Excel",[10,1824,1825],{},"Put the original notes, the extracted fields and the review list in one workbook, so the cleaning can be checked against its source:",[270,1827,1829],{"className":317,"code":1828,"language":319,"meta":275,"style":275},"with pd.ExcelWriter(\"tickets_extracted.xlsx\") as writer:\n    fields.merge(tickets[[\"ticket\", \"note\"]], on=\"ticket\").to_excel(writer, sheet_name=\"Tickets\", index=False)\n    lines[[\"ticket\", \"sku\"]].to_excel(writer, sheet_name=\"Ticket lines\", index=False)\n    missing[[\"ticket\", \"note\"]].to_excel(writer, sheet_name=\"Needs review\", index=False)\nprint(pd.ExcelFile(\"tickets_extracted.xlsx\").sheet_names)\n",[14,1830,1831,1849,1889,1920,1950],{"__ignoreMap":275},[279,1832,1833,1836,1839,1842,1844,1846],{"class":238,"line":281},[279,1834,1835],{"class":326},"with",[279,1837,1838],{"class":330}," pd.ExcelWriter(",[279,1840,1841],{"class":288},"\"tickets_extracted.xlsx\"",[279,1843,1780],{"class":330},[279,1845,342],{"class":326},[279,1847,1848],{"class":330}," writer:\n",[279,1850,1851,1854,1856,1858,1860,1862,1864,1866,1868,1871,1874,1876,1879,1881,1883,1885,1887],{"class":238,"line":334},[279,1852,1853],{"class":330},"    fields.merge(tickets[[",[279,1855,456],{"class":288},[279,1857,17],{"class":330},[279,1859,384],{"class":288},[279,1861,1706],{"class":330},[279,1863,1709],{"class":433},[279,1865,361],{"class":326},[279,1867,456],{"class":288},[279,1869,1870],{"class":330},").to_excel(writer, ",[279,1872,1873],{"class":433},"sheet_name",[279,1875,361],{"class":326},[279,1877,1878],{"class":288},"\"Tickets\"",[279,1880,17],{"class":330},[279,1882,610],{"class":433},[279,1884,361],{"class":326},[279,1886,615],{"class":396},[279,1888,370],{"class":330},[279,1890,1891,1894,1896,1898,1900,1903,1905,1907,1910,1912,1914,1916,1918],{"class":238,"line":348},[279,1892,1893],{"class":330},"    lines[[",[279,1895,456],{"class":288},[279,1897,17],{"class":330},[279,1899,890],{"class":288},[279,1901,1902],{"class":330},"]].to_excel(writer, ",[279,1904,1873],{"class":433},[279,1906,361],{"class":326},[279,1908,1909],{"class":288},"\"Ticket lines\"",[279,1911,17],{"class":330},[279,1913,610],{"class":433},[279,1915,361],{"class":326},[279,1917,615],{"class":396},[279,1919,370],{"class":330},[279,1921,1922,1925,1927,1929,1931,1933,1935,1937,1940,1942,1944,1946,1948],{"class":238,"line":355},[279,1923,1924],{"class":330},"    missing[[",[279,1926,456],{"class":288},[279,1928,17],{"class":330},[279,1930,384],{"class":288},[279,1932,1902],{"class":330},[279,1934,1873],{"class":433},[279,1936,361],{"class":326},[279,1938,1939],{"class":288},"\"Needs review\"",[279,1941,17],{"class":330},[279,1943,610],{"class":433},[279,1945,361],{"class":326},[279,1947,615],{"class":396},[279,1949,370],{"class":330},[279,1951,1952,1954,1957,1959],{"class":238,"line":373},[279,1953,450],{"class":396},[279,1955,1956],{"class":330},"(pd.ExcelFile(",[279,1958,1841],{"class":288},[279,1960,1961],{"class":330},").sheet_names)\n",[265,1963,1965],{"id":1964},"common-pitfalls","Common pitfalls",[1967,1968,1969,1983,1992,1998,2013],"ul",{},[1970,1971,1972,1063,1976,1979,1980,1982],"li",{},[1973,1974,1975],"strong",{},"Patterns looser than the format.",[14,1977,1978],{},"\\d+"," after ",[14,1981,404],{}," accepts typos; state the real length.",[1970,1984,1985,1988,1989,1991],{},[1973,1986,1987],{},"Forgetting case."," Add ",[14,1990,478],{}," and normalise the result.",[1970,1993,1994,1997],{},[1973,1995,1996],{},"One giant pattern."," Separate patterns per field are easier to test and fix.",[1970,1999,2000,2006,2007,641,2010,630],{},[1973,2001,2002,2003,2005],{},"Using ",[14,2004,494],{}," when a cell can hold several values."," Use ",[14,2008,2009],{},"extractall",[14,2011,2012],{},"findall",[1970,2014,2015,2018,2019,2022],{},[1973,2016,2017],{},"Leaving extracted numbers as text."," Convert with ",[14,2020,2021],{},"to_numeric"," straight away.",[265,2024,2026],{"id":2025},"performance-notes","Performance notes",[10,2028,2029,2031,2032,2035],{},[14,2030,263],{}," compiles the pattern once and runs it through the regular expression engine for each cell, handling hundreds of thousands of rows per second for simple patterns. Avoid patterns with nested repetition such as ",[14,2033,2034],{},"(\\w+\\s?)+",", which can take exponential time on long cells; anchored, specific patterns are both faster and more correct.",[265,2037,2039],{"id":2038},"conclusion","Conclusion",[10,2041,2042,2043,2045,2046,2048,2049,2051],{},"Use ",[14,2044,263],{}," with named groups for one value per cell, normalise the captured text into a canonical form immediately, and use separate, specific patterns for each field. Switch to ",[14,2047,1277],{}," when a cell can hold several values, validate code columns with ",[14,2050,1564],{},", convert extracted numbers to real types, and always list the rows where nothing matched — they tell you whether the data or the pattern needs fixing.",[265,2053,2055],{"id":2054},"frequently-asked-questions","Frequently asked questions",[10,2057,2058,2061],{},[1973,2059,2060],{},"What is the difference between str.extract and str.extractall?","\nstr.extract returns the first match in each cell as columns, one row per original row. str.extractall returns every match, with an extra index level numbering the matches within each cell, so one row of input can become several rows of output.",[10,2063,2064,2067,2068],{},[1973,2065,2066],{},"How do I name the extracted columns?","\nUse named groups in the pattern, written (?P",[2069,2070,2071],"name",{},"...). str.extract turns each named group into a column with that name, which is clearer than numbered columns 0, 1, 2.",[10,2073,2074,2077],{},[1973,2075,2076],{},"Why do I get NaN for some rows?","\nThose cells contain no text matching the pattern. That is useful information: list those rows and check whether the pattern is too strict or the cells genuinely lack the value.",[10,2079,2080,2083],{},[1973,2081,2082],{},"How do I match regardless of case?","\nPass flags=re.IGNORECASE to str.extract, or start the pattern with (?i). Then normalise the extracted value, for example with str.upper(), so \"po 10458\" and \"PO-10458\" end up identical.",[265,2085,2087],{"id":2086},"related","Related",[1967,2089,2090,2096,2103,2110],{},[1970,2091,2092,2093,2095],{},"Up one level: ",[27,2094,30],{"href":29}," — the full cleaning workflow.",[1970,2097,2098,2102],{},[27,2099,2101],{"href":2100},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fexcel-text-functions-left-right-mid-and-concat-in-pandas\u002F","Excel Text Functions LEFT, RIGHT, MID and CONCAT in pandas"," — position-based extraction.",[1970,2104,2105,2109],{},[27,2106,2108],{"href":2107},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Ffind-search-and-substitute-equivalents-in-pandas\u002F","FIND, SEARCH and SUBSTITUTE Equivalents in pandas"," — locating and replacing text.",[1970,2111,2112,2116],{},[27,2113,2115],{"href":2114},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fsplit-a-column-into-multiple-columns-with-pandas\u002F","Split a Column into Multiple Columns with pandas"," — when a delimiter does the job.",[2118,2119,2120],"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 .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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}",{"title":275,"searchDepth":334,"depth":334,"links":2122},[2123,2124,2125,2126,2127,2128,2129,2130,2131,2132,2133,2134,2135],{"id":267,"depth":334,"text":268},{"id":304,"depth":334,"text":305},{"id":498,"depth":334,"text":499},{"id":633,"depth":334,"text":634},{"id":1270,"depth":334,"text":1271},{"id":1553,"depth":334,"text":1554},{"id":1673,"depth":334,"text":1674},{"id":1821,"depth":334,"text":1822},{"id":1964,"depth":334,"text":1965},{"id":2025,"depth":334,"text":2026},{"id":2038,"depth":334,"text":2039},{"id":2054,"depth":334,"text":2055},{"id":2086,"depth":334,"text":2087},"2026-09-25","Pull order numbers, SKUs, quantities and amounts out of free-text cells like \"Re: PO-10442, 3 x SKU AB-1200 @ $12.50\". Use str.extract with named groups for one match per cell, extractall for many, fullmatch to validate codes, and a report of the rows where nothing matched.","md",[2140,2142,2144,2146],{"q":2060,"a":2141},"str.extract returns the first match in each cell as columns, one row per original row. str.extractall returns every match, with an extra index level numbering the matches within each cell, so one row of input can become several rows of output.",{"q":2066,"a":2143},"Use named groups in the pattern, written (?P\u003Cname>...). str.extract turns each named group into a column with that name, which is clearer than numbered columns 0, 1, 2.",{"q":2076,"a":2145},"Those cells contain no text matching the pattern. That is useful information: list those rows and check whether the pattern is too strict or the cells genuinely lack the value.",{"q":2082,"a":2147},"Pass flags=re.IGNORECASE to str.extract, or start the pattern with (?i). Then normalise the extracted value, for example with str.upper(), so \"po 10458\" and \"PO-10458\" end up identical.","extract numbers and codes from text with regex in pandas",null,{"breadcrumb":2151},[2152,2155,2158],{"name":2153,"item":2154},"Home","\u002F",{"name":2156,"item":2157},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":2159,"item":29},"Cleaning Excel Data With Pandas: A Production Workflow","\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fextract-numbers-and-codes-from-text-with-pandas",{"title":2162,"description":2163},"Extract Numbers, IDs and Codes from Text Columns in pandas (Regex)","Extract order numbers, product codes, quantities and prices from text with pandas str.extract and extractall: named groups, first versus all matches, case-insensitive patterns, validation with fullmatch and reporting cells with no match.","extract-numbers-and-codes-from-text-with-pandas","advanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fextract-numbers-and-codes-from-text-with-pandas\u002Findex",[2167,2168,2169],"data-cleaning","pandas","formula-equivalents","how-to","GFc6ZADrg2YmvfmscmgESD6d5p4lIkoYjMzbloylLoY",[2173,2177],{"title":2174,"path":2175,"stem":2176,"children":-1},"Detect Outliers in Excel Data with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fdetect-outliers-in-excel-data-with-pandas","advanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fdetect-outliers-in-excel-data-with-pandas\u002Findex",{"title":2178,"path":2179,"stem":2180,"children":-1},"Filter Excel Rows by Condition with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Ffilter-excel-rows-by-condition-with-pandas","advanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Ffilter-excel-rows-by-condition-with-pandas\u002Findex",[2182,2184],{"path":2183,"title":2159},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas",{"path":2185,"title":2156},"\u002Fadvanced-data-transformation-and-cleaning",[],[2188,2191,2194,2197,2200,2203],{"path":2189,"title":2190,"kind":2170},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-duplicate-rows-in-excel-with-python","Find Duplicate Rows in Excel with Python",{"path":2192,"title":2193,"kind":2170},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-duplicate-values-in-excel-with-openpyxl","Highlight Duplicate Values in Excel with openpyxl",{"path":2195,"title":2196,"kind":2170},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fclean-mixed-currency-amounts-into-amount-and-currency-columns","Clean Mixed-Currency Amounts into Amount and Currency Columns",{"path":2198,"title":2199,"kind":2170},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fcoalesce-several-excel-columns-into-one-with-pandas","Coalesce Several Excel Columns into One with pandas",{"path":2201,"title":2202,"kind":2170},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-currency-strings-to-numbers-with-pandas","Convert Currency Strings Like \"$1,234.56\" and \"(500)\" to Numbers with pandas",{"path":2204,"title":2205,"kind":2170},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-excel-text-columns-to-numbers-with-pandas","Convert Excel Text Columns to Numbers with pandas",1790972080670]