[{"data":1,"prerenderedAt":2501},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffuzzy-match-names-between-two-excel-files-with-python":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffuzzy-match-names-between-two-excel-files-with-python":2466,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffuzzy-match-names-between-two-excel-files-with-python":2474,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffuzzy-match-names-between-two-excel-files-with-python":2481,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffuzzy-match-names-between-two-excel-files-with-python":2482},{"id":4,"title":5,"body":6,"dateModified":2431,"datePublished":2431,"description":2432,"extension":2433,"faq":2434,"keyword":2443,"kind":2444,"meta":2445,"navigation":234,"path":2454,"seo":2455,"slug":2458,"stem":2459,"topics":2460,"type":2464,"__hash__":2465},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffuzzy-match-names-between-two-excel-files-with-python\u002Findex.md","Fuzzy Match Names Between Two Excel Files with Python",{"type":7,"value":8,"toc":2417},"minimark",[9,40,162,167,197,206,491,495,498,851,889,893,912,1075,1088,1177,1181,1195,1417,1441,1445,1448,1681,1694,1701,1793,1797,1806,1946,1953,1957,1960,2197,2205,2209,2309,2313,2329,2333,2342,2346,2353,2359,2365,2371,2375,2413],[10,11,12,13,17,18,21,22,25,26,29,30,35,36,39],"p",{},"The sales workbook says ",[14,15,16],"code",{},"Acme Ltd",", the finance workbook says ",[14,19,20],{},"ACME Limited",", and the CRM export says\n",[14,23,24],{},"Acme Ltd."," with a trailing full stop. A ",[14,27,28],{},"merge"," on the name column matches none of them, and VLOOKUP in Excel\ndoes no better. When there is no shared ID, the only way to join the files is to decide which names are close\nenough to be the same organisation — and to be honest about the ones that are not certain. This guide, part of\n",[31,32,34],"a",{"href":33},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002F","Merging and Joining Excel DataFrames",",\nbuilds a fuzzy join with the ",[14,37,38],{},"rapidfuzz"," library that is fast, tunable and reviewable.",[41,42,50,51,50,55,50,59,50,66,50,75,50,82,50,87,50,91,50,98,50,103,50,107,50,112,50,116,50,120,50,123,50,126,50,130,50,133,50,137,50,140,50,145,50,150,50,153,50,156],"svg",{"viewBox":43,"role":44,"ariaLabelledBy":45,"xmlns":48,"style":49},"0 0 760 156","img",[46,47],"fz-flow-t","fz-flow-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[52,53,54],"title",{"id":46},"A reviewable fuzzy join between two sheets",[56,57,58],"desc",{"id":47},"Normalise both name columns, score each left name against all right names, keep the best candidate with its score, accept matches above a high threshold, send the middle band for human review, and reject the rest.",[60,61],"rect",{"x":62,"y":62,"width":63,"height":64,"fill":65},"0","760","156","#ffffff",[60,67],{"x":68,"y":68,"width":69,"height":70,"rx":71,"fill":72,"stroke":73,"style":74},"20","150","86","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[76,77,81],"text",{"x":78,"y":79,"style":80},"95","50","font-size:14px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","normalise",[76,83,86],{"x":78,"y":84,"style":85},"72","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","case, punctuation,",[76,88,90],{"x":78,"y":89,"style":85},"92","legal suffixes",[92,93],"line",{"x1":94,"y1":95,"x2":96,"y2":95,"stroke":97,"style":74},"175","63","204","var(--brand,#5b5cf0)",[99,100],"polygon",{"points":101,"fill":102},"207,63 198,58 198,68","#5b5cf0",[60,104],{"x":105,"y":68,"width":69,"height":70,"rx":71,"fill":106,"stroke":97,"style":74},"210","#f0f4ff",[76,108,111],{"x":109,"y":79,"style":110},"285","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","score",[76,113,115],{"x":109,"y":84,"style":114},"font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","rapidfuzz scorer",[92,117],{"x1":118,"y1":95,"x2":119,"y2":95,"stroke":97,"style":74},"365","394",[99,121],{"points":122,"fill":102},"397,63 388,58 388,68",[60,124],{"x":125,"y":68,"width":69,"height":70,"rx":71,"fill":106,"stroke":97,"style":74},"400",[76,127,129],{"x":128,"y":79,"style":110},"475","best match",[76,131,132],{"x":128,"y":84,"style":114},"name + score",[92,134],{"x1":135,"y1":95,"x2":136,"y2":95,"stroke":97,"style":74},"555","584",[99,138],{"points":139,"fill":102},"587,63 578,58 578,68",[60,141],{"x":142,"y":68,"width":69,"height":70,"rx":71,"fill":143,"stroke":144,"style":74},"590","#d9f4f1","var(--teal,#0f9488)",[76,146,149],{"x":147,"y":79,"style":148},"665","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","decide",[76,151,152],{"x":147,"y":84,"style":114},"accept, review,",[76,154,155],{"x":147,"y":89,"style":114},"reject",[76,157,161],{"x":158,"y":159,"style":160},"380","140","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","the score is evidence, not a verdict; keep the doubtful band for a person",[163,164,166],"h2",{"id":165},"prerequisites","Prerequisites",[168,169,174],"pre",{"className":170,"code":171,"language":172,"meta":173,"style":173},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl rapidfuzz\n","bash","",[14,175,176],{"__ignoreMap":173},[177,178,180,184,188,191,194],"span",{"class":92,"line":179},1,[177,181,183],{"class":182},"sMTad","pip",[177,185,187],{"class":186},"srMev"," install",[177,189,190],{"class":186}," pandas",[177,192,193],{"class":186}," openpyxl",[177,195,196],{"class":186}," rapidfuzz\n",[10,198,199,201,202,205],{},[14,200,38],{}," is a fast, MIT-licensed string matching library with the same scorers people know from ",[14,203,204],{},"fuzzywuzzy",". The two example files:",[168,207,211],{"className":208,"code":209,"language":210,"meta":173,"style":173},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\npd.DataFrame({\n    \"Customer\": [\"Acme Ltd\", \"Globex Corporation\", \"Initech\", \"Umbrella Holdings plc\",\n                 \"Stark Industries\", \"Wayne Enterprises Inc.\"],\n    \"Revenue\": [120500, 98200, 43100, 87150, 150300, 64000],\n}).to_excel(\"sales.xlsx\", index=False)\n\npd.DataFrame({\n    \"Account name\": [\"ACME Limited\", \"Globex Corp.\", \"Initek Inc\", \"Umbrela Holdings\",\n                     \"Wayne Enterprise\", \"Soylent Green Co\"],\n    \"Credit limit\": [50000, 75000, 20000, 40000, 60000, 15000],\n}).to_excel(\"finance.xlsx\", index=False)\n\nsales = pd.read_excel(\"sales.xlsx\")\nfinance = pd.read_excel(\"finance.xlsx\")\n","python",[14,212,213,229,236,242,273,287,326,350,355,360,388,401,439,457,462,477],{"__ignoreMap":173},[177,214,215,219,223,226],{"class":92,"line":179},[177,216,218],{"class":217},"s-kum","import",[177,220,222],{"class":221},"skGVy"," pandas ",[177,224,225],{"class":217},"as",[177,227,228],{"class":221}," pd\n",[177,230,232],{"class":92,"line":231},2,[177,233,235],{"emptyLinePlaceholder":234},true,"\n",[177,237,239],{"class":92,"line":238},3,[177,240,241],{"class":221},"pd.DataFrame({\n",[177,243,245,248,251,254,257,260,262,265,267,270],{"class":92,"line":244},4,[177,246,247],{"class":186},"    \"Customer\"",[177,249,250],{"class":221},": [",[177,252,253],{"class":186},"\"Acme Ltd\"",[177,255,256],{"class":221},", ",[177,258,259],{"class":186},"\"Globex Corporation\"",[177,261,256],{"class":221},[177,263,264],{"class":186},"\"Initech\"",[177,266,256],{"class":221},[177,268,269],{"class":186},"\"Umbrella Holdings plc\"",[177,271,272],{"class":221},",\n",[177,274,276,279,281,284],{"class":92,"line":275},5,[177,277,278],{"class":186},"                 \"Stark Industries\"",[177,280,256],{"class":221},[177,282,283],{"class":186},"\"Wayne Enterprises Inc.\"",[177,285,286],{"class":221},"],\n",[177,288,290,293,295,299,301,304,306,309,311,314,316,319,321,324],{"class":92,"line":289},6,[177,291,292],{"class":186},"    \"Revenue\"",[177,294,250],{"class":221},[177,296,298],{"class":297},"sP0c6","120500",[177,300,256],{"class":221},[177,302,303],{"class":297},"98200",[177,305,256],{"class":221},[177,307,308],{"class":297},"43100",[177,310,256],{"class":221},[177,312,313],{"class":297},"87150",[177,315,256],{"class":221},[177,317,318],{"class":297},"150300",[177,320,256],{"class":221},[177,322,323],{"class":297},"64000",[177,325,286],{"class":221},[177,327,329,332,335,337,341,344,347],{"class":92,"line":328},7,[177,330,331],{"class":221},"}).to_excel(",[177,333,334],{"class":186},"\"sales.xlsx\"",[177,336,256],{"class":221},[177,338,340],{"class":339},"sa561","index",[177,342,343],{"class":217},"=",[177,345,346],{"class":297},"False",[177,348,349],{"class":221},")\n",[177,351,353],{"class":92,"line":352},8,[177,354,235],{"emptyLinePlaceholder":234},[177,356,358],{"class":92,"line":357},9,[177,359,241],{"class":221},[177,361,363,366,368,371,373,376,378,381,383,386],{"class":92,"line":362},10,[177,364,365],{"class":186},"    \"Account name\"",[177,367,250],{"class":221},[177,369,370],{"class":186},"\"ACME Limited\"",[177,372,256],{"class":221},[177,374,375],{"class":186},"\"Globex Corp.\"",[177,377,256],{"class":221},[177,379,380],{"class":186},"\"Initek Inc\"",[177,382,256],{"class":221},[177,384,385],{"class":186},"\"Umbrela Holdings\"",[177,387,272],{"class":221},[177,389,391,394,396,399],{"class":92,"line":390},11,[177,392,393],{"class":186},"                     \"Wayne Enterprise\"",[177,395,256],{"class":221},[177,397,398],{"class":186},"\"Soylent Green Co\"",[177,400,286],{"class":221},[177,402,404,407,409,412,414,417,419,422,424,427,429,432,434,437],{"class":92,"line":403},12,[177,405,406],{"class":186},"    \"Credit limit\"",[177,408,250],{"class":221},[177,410,411],{"class":297},"50000",[177,413,256],{"class":221},[177,415,416],{"class":297},"75000",[177,418,256],{"class":221},[177,420,421],{"class":297},"20000",[177,423,256],{"class":221},[177,425,426],{"class":297},"40000",[177,428,256],{"class":221},[177,430,431],{"class":297},"60000",[177,433,256],{"class":221},[177,435,436],{"class":297},"15000",[177,438,286],{"class":221},[177,440,442,444,447,449,451,453,455],{"class":92,"line":441},13,[177,443,331],{"class":221},[177,445,446],{"class":186},"\"finance.xlsx\"",[177,448,256],{"class":221},[177,450,340],{"class":339},[177,452,343],{"class":217},[177,454,346],{"class":297},[177,456,349],{"class":221},[177,458,460],{"class":92,"line":459},14,[177,461,235],{"emptyLinePlaceholder":234},[177,463,465,468,470,473,475],{"class":92,"line":464},15,[177,466,467],{"class":221},"sales ",[177,469,343],{"class":217},[177,471,472],{"class":221}," pd.read_excel(",[177,474,334],{"class":186},[177,476,349],{"class":221},[177,478,480,483,485,487,489],{"class":92,"line":479},16,[177,481,482],{"class":221},"finance ",[177,484,343],{"class":217},[177,486,472],{"class":221},[177,488,446],{"class":186},[177,490,349],{"class":221},[163,492,494],{"id":493},"step-1-normalise-before-you-compare","Step 1 — Normalise before you compare",[10,496,497],{},"Most of the difference between two spellings of a company name is noise: capitalisation, punctuation and legal suffixes. Removing it first makes exact matches\nout of many pairs and leaves the fuzzy scorer to handle only genuine variation. Keep the original column and add a normalised key beside it.",[168,499,501],{"className":208,"code":500,"language":210,"meta":173,"style":173},"import re\n\nSUFFIXES = r\"\\b(ltd|limited|inc|incorporated|corp|corporation|co|company|plc|llc|gmbh)\\b\"\n\ndef normalise(name: str) -> str:\n    text = str(name).lower()\n    text = re.sub(r\"[^\\w\\s]\", \" \", text)          # punctuation to spaces\n    text = re.sub(SUFFIXES, \" \", text)            # legal forms add nothing\n    return re.sub(r\"\\s+\", \" \", text).strip()\n\nsales[\"key\"] = sales[\"Customer\"].map(normalise)\nfinance[\"key\"] = finance[\"Account name\"].map(normalise)\nprint(sales[[\"Customer\", \"key\"]])\n\nexact = sales.merge(finance, on=\"key\", how=\"inner\")\nprint(f\"{len(exact)} of {len(sales)} match exactly after normalising\")\n",[14,502,503,510,514,591,595,618,631,668,688,714,718,740,759,776,780,809],{"__ignoreMap":173},[177,504,505,507],{"class":92,"line":179},[177,506,218],{"class":217},[177,508,509],{"class":221}," re\n",[177,511,512],{"class":92,"line":231},[177,513,235],{"emptyLinePlaceholder":234},[177,515,516,519,522,525,528,531,534,537,540,542,545,547,550,552,555,557,560,562,565,567,570,572,575,577,580,582,585,588],{"class":92,"line":238},[177,517,518],{"class":297},"SUFFIXES",[177,520,521],{"class":217}," =",[177,523,524],{"class":217}," r",[177,526,527],{"class":186},"\"",[177,529,530],{"class":297},"\\b(",[177,532,533],{"class":186},"ltd",[177,535,536],{"class":217},"|",[177,538,539],{"class":186},"limited",[177,541,536],{"class":217},[177,543,544],{"class":186},"inc",[177,546,536],{"class":217},[177,548,549],{"class":186},"incorporated",[177,551,536],{"class":217},[177,553,554],{"class":186},"corp",[177,556,536],{"class":217},[177,558,559],{"class":186},"corporation",[177,561,536],{"class":217},[177,563,564],{"class":186},"co",[177,566,536],{"class":217},[177,568,569],{"class":186},"company",[177,571,536],{"class":217},[177,573,574],{"class":186},"plc",[177,576,536],{"class":217},[177,578,579],{"class":186},"llc",[177,581,536],{"class":217},[177,583,584],{"class":186},"gmbh",[177,586,587],{"class":297},")\\b",[177,589,590],{"class":186},"\"\n",[177,592,593],{"class":92,"line":244},[177,594,235],{"emptyLinePlaceholder":234},[177,596,597,600,604,607,610,613,615],{"class":92,"line":275},[177,598,599],{"class":217},"def",[177,601,603],{"class":602},"s_Opv"," normalise",[177,605,606],{"class":221},"(name: ",[177,608,609],{"class":297},"str",[177,611,612],{"class":221},") -> ",[177,614,609],{"class":297},[177,616,617],{"class":221},":\n",[177,619,620,623,625,628],{"class":92,"line":289},[177,621,622],{"class":221},"    text ",[177,624,343],{"class":217},[177,626,627],{"class":297}," str",[177,629,630],{"class":221},"(name).lower()\n",[177,632,633,635,637,640,643,645,648,651,654,656,658,661,664],{"class":92,"line":328},[177,634,622],{"class":221},[177,636,343],{"class":217},[177,638,639],{"class":221}," re.sub(",[177,641,642],{"class":217},"r",[177,644,527],{"class":186},[177,646,647],{"class":297},"[",[177,649,650],{"class":217},"^",[177,652,653],{"class":297},"\\w\\s]",[177,655,527],{"class":186},[177,657,256],{"class":221},[177,659,660],{"class":186},"\" \"",[177,662,663],{"class":221},", text)          ",[177,665,667],{"class":666},"s-wDw","# punctuation to spaces\n",[177,669,670,672,674,676,678,680,682,685],{"class":92,"line":352},[177,671,622],{"class":221},[177,673,343],{"class":217},[177,675,639],{"class":221},[177,677,518],{"class":297},[177,679,256],{"class":221},[177,681,660],{"class":186},[177,683,684],{"class":221},", text)            ",[177,686,687],{"class":666},"# legal forms add nothing\n",[177,689,690,693,695,697,699,702,705,707,709,711],{"class":92,"line":357},[177,691,692],{"class":217},"    return",[177,694,639],{"class":221},[177,696,642],{"class":217},[177,698,527],{"class":186},[177,700,701],{"class":297},"\\s",[177,703,704],{"class":217},"+",[177,706,527],{"class":186},[177,708,256],{"class":221},[177,710,660],{"class":186},[177,712,713],{"class":221},", text).strip()\n",[177,715,716],{"class":92,"line":362},[177,717,235],{"emptyLinePlaceholder":234},[177,719,720,723,726,729,731,734,737],{"class":92,"line":390},[177,721,722],{"class":221},"sales[",[177,724,725],{"class":186},"\"key\"",[177,727,728],{"class":221},"] ",[177,730,343],{"class":217},[177,732,733],{"class":221}," sales[",[177,735,736],{"class":186},"\"Customer\"",[177,738,739],{"class":221},"].map(normalise)\n",[177,741,742,745,747,749,751,754,757],{"class":92,"line":403},[177,743,744],{"class":221},"finance[",[177,746,725],{"class":186},[177,748,728],{"class":221},[177,750,343],{"class":217},[177,752,753],{"class":221}," finance[",[177,755,756],{"class":186},"\"Account name\"",[177,758,739],{"class":221},[177,760,761,764,767,769,771,773],{"class":92,"line":441},[177,762,763],{"class":297},"print",[177,765,766],{"class":221},"(sales[[",[177,768,736],{"class":186},[177,770,256],{"class":221},[177,772,725],{"class":186},[177,774,775],{"class":221},"]])\n",[177,777,778],{"class":92,"line":459},[177,779,235],{"emptyLinePlaceholder":234},[177,781,782,785,787,790,793,795,797,799,802,804,807],{"class":92,"line":464},[177,783,784],{"class":221},"exact ",[177,786,343],{"class":217},[177,788,789],{"class":221}," sales.merge(finance, ",[177,791,792],{"class":339},"on",[177,794,343],{"class":217},[177,796,725],{"class":186},[177,798,256],{"class":221},[177,800,801],{"class":339},"how",[177,803,343],{"class":217},[177,805,806],{"class":186},"\"inner\"",[177,808,349],{"class":221},[177,810,811,813,816,819,821,825,828,831,834,837,839,841,844,846,849],{"class":92,"line":479},[177,812,763],{"class":297},[177,814,815],{"class":221},"(",[177,817,818],{"class":217},"f",[177,820,527],{"class":186},[177,822,824],{"class":823},"sSjpA","{",[177,826,827],{"class":297},"len",[177,829,830],{"class":221},"(exact)",[177,832,833],{"class":823},"}",[177,835,836],{"class":186}," of ",[177,838,824],{"class":823},[177,840,827],{"class":297},[177,842,843],{"class":221},"(sales)",[177,845,833],{"class":823},[177,847,848],{"class":186}," match exactly after normalising\"",[177,850,349],{"class":221},[10,852,853,854,856,857,859,860,856,863,866,867,256,870,256,873,876,877,880,881,884,885,888],{},"On this data normalisation alone joins two of the six customers — ",[14,855,16],{}," with ",[14,858,20],{}," and ",[14,861,862],{},"Globex Corporation",[14,864,865],{},"Globex Corp."," — leaving\nthe typos (",[14,868,869],{},"Initek",[14,871,872],{},"Umbrela",[14,874,875],{},"Wayne Enterprise",") and the genuinely absent ",[14,878,879],{},"Stark Industries"," for the fuzzy step. Tailor the suffix list to your data — adding words such as ",[14,882,883],{},"group"," or ",[14,886,887],{},"holdings"," helps some files and\nhurts others where those words distinguish two real companies.",[163,890,892],{"id":891},"step-2-choose-a-scorer","Step 2 — Choose a scorer",[10,894,895,896,899,900,903,904,907,908,911],{},"rapidfuzz offers several ways to score similarity from 0 to 100, and they disagree in instructive ways. ",[14,897,898],{},"ratio"," compares whole strings character by character;\n",[14,901,902],{},"token_sort_ratio"," ignores word order; ",[14,905,906],{},"token_set_ratio"," ignores extra words; ",[14,909,910],{},"WRatio"," combines several and is a sensible default for names.",[168,913,915],{"className":208,"code":914,"language":210,"meta":173,"style":173},"from rapidfuzz import fuzz\n\npairs = [(\"acme\", \"acme\"), (\"globex\", \"globex\"), (\"umbrella holdings\", \"umbrella\"),\n         (\"stark industries\", \"industries stark\"), (\"wayne enterprises\", \"wayne enterprise\")]\nfor left, right in pairs:\n    print(f\"{left!r:22} {right!r:22}\",\n          round(fuzz.ratio(left, right)), round(fuzz.token_sort_ratio(left, right)),\n          round(fuzz.token_set_ratio(left, right)), round(fuzz.WRatio(left, right)))\n",[14,916,917,930,934,974,1000,1014,1049,1063],{"__ignoreMap":173},[177,918,919,922,925,927],{"class":92,"line":179},[177,920,921],{"class":217},"from",[177,923,924],{"class":221}," rapidfuzz ",[177,926,218],{"class":217},[177,928,929],{"class":221}," fuzz\n",[177,931,932],{"class":92,"line":231},[177,933,235],{"emptyLinePlaceholder":234},[177,935,936,939,941,944,947,949,951,954,957,959,961,963,966,968,971],{"class":92,"line":238},[177,937,938],{"class":221},"pairs ",[177,940,343],{"class":217},[177,942,943],{"class":221}," [(",[177,945,946],{"class":186},"\"acme\"",[177,948,256],{"class":221},[177,950,946],{"class":186},[177,952,953],{"class":221},"), (",[177,955,956],{"class":186},"\"globex\"",[177,958,256],{"class":221},[177,960,956],{"class":186},[177,962,953],{"class":221},[177,964,965],{"class":186},"\"umbrella holdings\"",[177,967,256],{"class":221},[177,969,970],{"class":186},"\"umbrella\"",[177,972,973],{"class":221},"),\n",[177,975,976,979,982,984,987,989,992,994,997],{"class":92,"line":244},[177,977,978],{"class":221},"         (",[177,980,981],{"class":186},"\"stark industries\"",[177,983,256],{"class":221},[177,985,986],{"class":186},"\"industries stark\"",[177,988,953],{"class":221},[177,990,991],{"class":186},"\"wayne enterprises\"",[177,993,256],{"class":221},[177,995,996],{"class":186},"\"wayne enterprise\"",[177,998,999],{"class":221},")]\n",[177,1001,1002,1005,1008,1011],{"class":92,"line":275},[177,1003,1004],{"class":217},"for",[177,1006,1007],{"class":221}," left, right ",[177,1009,1010],{"class":217},"in",[177,1012,1013],{"class":221}," pairs:\n",[177,1015,1016,1019,1021,1023,1025,1027,1030,1033,1035,1038,1041,1043,1045,1047],{"class":92,"line":289},[177,1017,1018],{"class":297},"    print",[177,1020,815],{"class":221},[177,1022,818],{"class":217},[177,1024,527],{"class":186},[177,1026,824],{"class":823},[177,1028,1029],{"class":221},"left",[177,1031,1032],{"class":217},"!r:22",[177,1034,833],{"class":823},[177,1036,1037],{"class":823}," {",[177,1039,1040],{"class":221},"right",[177,1042,1032],{"class":217},[177,1044,833],{"class":823},[177,1046,527],{"class":186},[177,1048,272],{"class":221},[177,1050,1051,1054,1057,1060],{"class":92,"line":328},[177,1052,1053],{"class":297},"          round",[177,1055,1056],{"class":221},"(fuzz.ratio(left, right)), ",[177,1058,1059],{"class":297},"round",[177,1061,1062],{"class":221},"(fuzz.token_sort_ratio(left, right)),\n",[177,1064,1065,1067,1070,1072],{"class":92,"line":352},[177,1066,1053],{"class":297},[177,1068,1069],{"class":221},"(fuzz.token_set_ratio(left, right)), ",[177,1071,1059],{"class":297},[177,1073,1074],{"class":221},"(fuzz.WRatio(left, right)))\n",[10,1076,1077,1079,1080,1083,1084,1087],{},[14,1078,906],{}," scores ",[14,1081,1082],{},"umbrella holdings"," against ",[14,1085,1086],{},"umbrella"," as a perfect match, because one set of words contains the other — useful for names with extra words, and\ndangerous for short names that are contained in many others. Try the scorers on a sample of known pairs from your own data before choosing one.",[41,1089,50,1094,50,1097,50,1100,50,1103,50,1108,50,1114,50,1120,50,1125,50,1129,50,1133,50,1136,50,1139,50,1143,50,1146,50,1149,50,1151,50,1156,50,1160,50,1164,50,1167,50,1170,50,1173],{"viewBox":1090,"role":44,"ariaLabelledBy":1091,"xmlns":48,"style":49},"0 0 760 191",[1092,1093],"fz-scorers-t","fz-scorers-d",[52,1095,1096],{"id":1092},"What each rapidfuzz scorer forgives",[56,1098,1099],{"id":1093},"ratio forgives small typing differences only. token_sort_ratio also forgives a different word order. token_set_ratio also forgives extra words on one side. WRatio blends these and is a good default for names.",[60,1101],{"x":62,"y":62,"width":63,"height":1102,"fill":65},"191",[60,1104],{"x":68,"y":68,"width":1105,"height":1106,"rx":1107,"fill":72,"stroke":73,"style":74},"226.7","127","14",[76,1109,1113],{"x":1110,"y":1111,"style":1112},"133.3","46","font-size:13px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","fuzz.ratio",[92,1115],{"x1":1116,"y1":1117,"x2":1118,"y2":1117,"stroke":73,"style":1119},"36","56","230.7","stroke-width:1px",[76,1121,1124],{"x":1110,"y":1122,"style":1123},"78","font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","typos only",[76,1126,1128],{"x":1110,"y":1127,"style":1123},"101","order matters",[76,1130,1132],{"x":1110,"y":1131,"style":1123},"124","extra words hurt",[60,1134],{"x":1135,"y":68,"width":1105,"height":1106,"rx":1107,"fill":106,"stroke":97,"style":74},"266.7",[76,1137,902],{"x":158,"y":1111,"style":1138},"font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[92,1140],{"x1":1141,"y1":1117,"x2":1142,"y2":1117,"stroke":97,"style":1119},"282.7","477.3",[76,1144,1145],{"x":158,"y":1122,"style":1123},"typos",[76,1147,1148],{"x":158,"y":1127,"style":1123},"any word order",[76,1150,1132],{"x":158,"y":1131,"style":1123},[60,1152],{"x":1153,"y":68,"width":1105,"height":1106,"rx":1107,"fill":1154,"stroke":1155,"style":74},"513.3","#fdefd8","var(--gold,#b4740a)",[76,1157,906],{"x":1158,"y":1111,"style":1159},"626.7","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[92,1161],{"x1":1162,"y1":1117,"x2":1163,"y2":1117,"stroke":1155,"style":1119},"529.3","724",[76,1165,1166],{"x":1158,"y":1122,"style":1123},"typos, any order",[76,1168,1169],{"x":1158,"y":1127,"style":1123},"extra words ignored",[76,1171,1172],{"x":1158,"y":1131,"style":1123},"short names risky",[76,1174,1176],{"x":158,"y":1175,"style":160},"177","WRatio combines them; test on known pairs first",[163,1178,1180],{"id":1179},"step-3-find-the-best-match-for-each-name","Step 3 — Find the best match for each name",[10,1182,1183,1186,1187,1190,1191,1194],{},[14,1184,1185],{},"process.extractOne"," scores one query against a list of choices and returns the best choice, its score and its position. A ",[14,1188,1189],{},"score_cutoff"," returns ",[14,1192,1193],{},"None"," when nothing is\ngood enough, which is cleaner than filtering afterwards.",[168,1196,1198],{"className":208,"code":1197,"language":210,"meta":173,"style":173},"from rapidfuzz import process\n\nchoices = finance[\"key\"].tolist()\n\ndef best_match(key: str, cutoff: float = 75):\n    found = process.extractOne(key, choices, scorer=fuzz.WRatio, score_cutoff=cutoff)\n    if found is None:\n        return pd.Series({\"match_key\": None, \"score\": 0.0, \"finance_row\": None})\n    choice, score, index = found\n    return pd.Series({\"match_key\": choice, \"score\": round(score, 1), \"finance_row\": index})\n\nmatches = sales.join(sales[\"key\"].apply(best_match))\nprint(matches[[\"Customer\", \"match_key\", \"score\"]])\n",[14,1199,1200,1211,1215,1229,1233,1259,1284,1300,1338,1348,1379,1383,1398],{"__ignoreMap":173},[177,1201,1202,1204,1206,1208],{"class":92,"line":179},[177,1203,921],{"class":217},[177,1205,924],{"class":221},[177,1207,218],{"class":217},[177,1209,1210],{"class":221}," process\n",[177,1212,1213],{"class":92,"line":231},[177,1214,235],{"emptyLinePlaceholder":234},[177,1216,1217,1220,1222,1224,1226],{"class":92,"line":238},[177,1218,1219],{"class":221},"choices ",[177,1221,343],{"class":217},[177,1223,753],{"class":221},[177,1225,725],{"class":186},[177,1227,1228],{"class":221},"].tolist()\n",[177,1230,1231],{"class":92,"line":244},[177,1232,235],{"emptyLinePlaceholder":234},[177,1234,1235,1237,1240,1243,1245,1248,1251,1253,1256],{"class":92,"line":275},[177,1236,599],{"class":217},[177,1238,1239],{"class":602}," best_match",[177,1241,1242],{"class":221},"(key: ",[177,1244,609],{"class":297},[177,1246,1247],{"class":221},", cutoff: ",[177,1249,1250],{"class":297},"float",[177,1252,521],{"class":217},[177,1254,1255],{"class":297}," 75",[177,1257,1258],{"class":221},"):\n",[177,1260,1261,1264,1266,1269,1272,1274,1277,1279,1281],{"class":92,"line":289},[177,1262,1263],{"class":221},"    found ",[177,1265,343],{"class":217},[177,1267,1268],{"class":221}," process.extractOne(key, choices, ",[177,1270,1271],{"class":339},"scorer",[177,1273,343],{"class":217},[177,1275,1276],{"class":221},"fuzz.WRatio, ",[177,1278,1189],{"class":339},[177,1280,343],{"class":217},[177,1282,1283],{"class":221},"cutoff)\n",[177,1285,1286,1289,1292,1295,1298],{"class":92,"line":328},[177,1287,1288],{"class":217},"    if",[177,1290,1291],{"class":221}," found ",[177,1293,1294],{"class":217},"is",[177,1296,1297],{"class":297}," None",[177,1299,617],{"class":221},[177,1301,1302,1305,1308,1311,1314,1316,1318,1321,1323,1326,1328,1331,1333,1335],{"class":92,"line":352},[177,1303,1304],{"class":217},"        return",[177,1306,1307],{"class":221}," pd.Series({",[177,1309,1310],{"class":186},"\"match_key\"",[177,1312,1313],{"class":221},": ",[177,1315,1193],{"class":297},[177,1317,256],{"class":221},[177,1319,1320],{"class":186},"\"score\"",[177,1322,1313],{"class":221},[177,1324,1325],{"class":297},"0.0",[177,1327,256],{"class":221},[177,1329,1330],{"class":186},"\"finance_row\"",[177,1332,1313],{"class":221},[177,1334,1193],{"class":297},[177,1336,1337],{"class":221},"})\n",[177,1339,1340,1343,1345],{"class":92,"line":357},[177,1341,1342],{"class":221},"    choice, score, index ",[177,1344,343],{"class":217},[177,1346,1347],{"class":221}," found\n",[177,1349,1350,1352,1354,1356,1359,1361,1363,1365,1368,1371,1374,1376],{"class":92,"line":362},[177,1351,692],{"class":217},[177,1353,1307],{"class":221},[177,1355,1310],{"class":186},[177,1357,1358],{"class":221},": choice, ",[177,1360,1320],{"class":186},[177,1362,1313],{"class":221},[177,1364,1059],{"class":297},[177,1366,1367],{"class":221},"(score, ",[177,1369,1370],{"class":297},"1",[177,1372,1373],{"class":221},"), ",[177,1375,1330],{"class":186},[177,1377,1378],{"class":221},": index})\n",[177,1380,1381],{"class":92,"line":390},[177,1382,235],{"emptyLinePlaceholder":234},[177,1384,1385,1388,1390,1393,1395],{"class":92,"line":403},[177,1386,1387],{"class":221},"matches ",[177,1389,343],{"class":217},[177,1391,1392],{"class":221}," sales.join(sales[",[177,1394,725],{"class":186},[177,1396,1397],{"class":221},"].apply(best_match))\n",[177,1399,1400,1402,1405,1407,1409,1411,1413,1415],{"class":92,"line":441},[177,1401,763],{"class":297},[177,1403,1404],{"class":221},"(matches[[",[177,1406,736],{"class":186},[177,1408,256],{"class":221},[177,1410,1310],{"class":186},[177,1412,256],{"class":221},[177,1414,1320],{"class":186},[177,1416,775],{"class":221},[10,1418,1419,859,1422,1425,1426,1429,1430,1432,1433,1435,1436,1440],{},[14,1420,1421],{},"Umbrella Holdings plc",[14,1423,1424],{},"Wayne Enterprises Inc."," find their misspelt counterparts with scores of 97, ",[14,1427,1428],{},"Initech"," reaches ",[14,1431,869],{}," at about 77, and\n",[14,1434,879],{}," has no counterpart in the finance file — with a cut-off of 75 it correctly stays unmatched instead of being paired with the least-bad candidate.\nWithout a cut-off, every name gets ",[1437,1438,1439],"em",{},"some"," match — the single most common way fuzzy joins produce confident nonsense.",[163,1442,1444],{"id":1443},"step-4-accept-review-or-reject-by-score-band","Step 4 — Accept, review or reject by score band",[10,1446,1447],{},"One threshold forces a choice between missing real matches and accepting false ones. Two thresholds do better: accept high scores automatically, reject low scores, and\nsend the band in between to a person. The bands below suit company names; calibrate them on your own data.",[168,1449,1451],{"className":208,"code":1450,"language":210,"meta":173,"style":173},"ACCEPT, REVIEW = 92, 75\n\ndef decision(score: float) -> str:\n    if score >= ACCEPT:\n        return \"accepted\"\n    if score >= REVIEW:\n        return \"review\"\n    return \"no match\"\n\nmatches[\"decision\"] = matches[\"score\"].map(decision)\njoined = matches.merge(\n    finance.reset_index().rename(columns={\"index\": \"finance_row\"}),\n    on=\"finance_row\", how=\"left\", suffixes=(\"\", \"_finance\"),\n)\nprint(joined[[\"Customer\", \"Account name\", \"score\", \"decision\", \"Credit limit\"]])\nprint(joined[\"decision\"].value_counts())\n",[14,1452,1453,1473,1477,1495,1510,1517,1530,1537,1544,1548,1568,1578,1600,1637,1641,1669],{"__ignoreMap":173},[177,1454,1455,1458,1460,1463,1465,1468,1470],{"class":92,"line":179},[177,1456,1457],{"class":297},"ACCEPT",[177,1459,256],{"class":221},[177,1461,1462],{"class":297},"REVIEW",[177,1464,521],{"class":217},[177,1466,1467],{"class":297}," 92",[177,1469,256],{"class":221},[177,1471,1472],{"class":297},"75\n",[177,1474,1475],{"class":92,"line":231},[177,1476,235],{"emptyLinePlaceholder":234},[177,1478,1479,1481,1484,1487,1489,1491,1493],{"class":92,"line":238},[177,1480,599],{"class":217},[177,1482,1483],{"class":602}," decision",[177,1485,1486],{"class":221},"(score: ",[177,1488,1250],{"class":297},[177,1490,612],{"class":221},[177,1492,609],{"class":297},[177,1494,617],{"class":221},[177,1496,1497,1499,1502,1505,1508],{"class":92,"line":244},[177,1498,1288],{"class":217},[177,1500,1501],{"class":221}," score ",[177,1503,1504],{"class":217},">=",[177,1506,1507],{"class":297}," ACCEPT",[177,1509,617],{"class":221},[177,1511,1512,1514],{"class":92,"line":275},[177,1513,1304],{"class":217},[177,1515,1516],{"class":186}," \"accepted\"\n",[177,1518,1519,1521,1523,1525,1528],{"class":92,"line":289},[177,1520,1288],{"class":217},[177,1522,1501],{"class":221},[177,1524,1504],{"class":217},[177,1526,1527],{"class":297}," REVIEW",[177,1529,617],{"class":221},[177,1531,1532,1534],{"class":92,"line":328},[177,1533,1304],{"class":217},[177,1535,1536],{"class":186}," \"review\"\n",[177,1538,1539,1541],{"class":92,"line":352},[177,1540,692],{"class":217},[177,1542,1543],{"class":186}," \"no match\"\n",[177,1545,1546],{"class":92,"line":357},[177,1547,235],{"emptyLinePlaceholder":234},[177,1549,1550,1553,1556,1558,1560,1563,1565],{"class":92,"line":362},[177,1551,1552],{"class":221},"matches[",[177,1554,1555],{"class":186},"\"decision\"",[177,1557,728],{"class":221},[177,1559,343],{"class":217},[177,1561,1562],{"class":221}," matches[",[177,1564,1320],{"class":186},[177,1566,1567],{"class":221},"].map(decision)\n",[177,1569,1570,1573,1575],{"class":92,"line":390},[177,1571,1572],{"class":221},"joined ",[177,1574,343],{"class":217},[177,1576,1577],{"class":221}," matches.merge(\n",[177,1579,1580,1583,1586,1588,1590,1593,1595,1597],{"class":92,"line":403},[177,1581,1582],{"class":221},"    finance.reset_index().rename(",[177,1584,1585],{"class":339},"columns",[177,1587,343],{"class":217},[177,1589,824],{"class":221},[177,1591,1592],{"class":186},"\"index\"",[177,1594,1313],{"class":221},[177,1596,1330],{"class":186},[177,1598,1599],{"class":221},"}),\n",[177,1601,1602,1605,1607,1609,1611,1613,1615,1618,1620,1623,1625,1627,1630,1632,1635],{"class":92,"line":441},[177,1603,1604],{"class":339},"    on",[177,1606,343],{"class":217},[177,1608,1330],{"class":186},[177,1610,256],{"class":221},[177,1612,801],{"class":339},[177,1614,343],{"class":217},[177,1616,1617],{"class":186},"\"left\"",[177,1619,256],{"class":221},[177,1621,1622],{"class":339},"suffixes",[177,1624,343],{"class":217},[177,1626,815],{"class":221},[177,1628,1629],{"class":186},"\"\"",[177,1631,256],{"class":221},[177,1633,1634],{"class":186},"\"_finance\"",[177,1636,973],{"class":221},[177,1638,1639],{"class":92,"line":459},[177,1640,349],{"class":221},[177,1642,1643,1645,1648,1650,1652,1654,1656,1658,1660,1662,1664,1667],{"class":92,"line":464},[177,1644,763],{"class":297},[177,1646,1647],{"class":221},"(joined[[",[177,1649,736],{"class":186},[177,1651,256],{"class":221},[177,1653,756],{"class":186},[177,1655,256],{"class":221},[177,1657,1320],{"class":186},[177,1659,256],{"class":221},[177,1661,1555],{"class":186},[177,1663,256],{"class":221},[177,1665,1666],{"class":186},"\"Credit limit\"",[177,1668,775],{"class":221},[177,1670,1671,1673,1676,1678],{"class":92,"line":479},[177,1672,763],{"class":297},[177,1674,1675],{"class":221},"(joined[",[177,1677,1555],{"class":186},[177,1679,1680],{"class":221},"].value_counts())\n",[10,1682,1683,1684,1686,1687,1690,1691,1693],{},"Here ",[14,1685,1428],{}," lands in the review band: ",[14,1688,1689],{},"Initek Inc"," is probably the same company with a typo, but a score in the seventies is also what two different\nshort names can produce, so a person should confirm it. The other four matches are accepted and ",[14,1692,879],{}," is reported as unmatched.",[10,1695,1696,1697,1700],{},"Keeping the finance row position from ",[14,1698,1699],{},"extractOne"," and joining on it — rather than joining on the matched text — avoids a subtle bug when two finance rows normalise to the\nsame key.",[41,1702,50,1708,50,1711,50,1714,50,1718,50,1723,50,1728,50,1734,50,1739,50,1746,50,1751,50,1754,50,1758,50,1761,50,1764,50,1768,50,1771,50,1775,50,1778,50,1784,50,1788],{"viewBox":1703,"role":44,"ariaLabelledBy":1704,"xmlns":48,"style":1707},"0 0 720 212",[1705,1706],"fz-bands-t","fz-bands-d","width:100%;max-width:720px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[52,1709,1710],{"id":1705},"Score bands for automatic and manual decisions",[56,1712,1713],{"id":1706},"Scores of 92 and above are accepted automatically, scores from 75 to 92 go to a review sheet, and scores below 75 are treated as no match.",[60,1715],{"x":62,"y":62,"width":1716,"height":1717,"fill":65},"720","212",[76,1719,1722],{"x":68,"y":1720,"style":1721},"30","font-size:12px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start","WRatio score and what happens to the pair",[60,1724],{"x":68,"y":1111,"width":1725,"height":1726,"rx":1727,"fill":65,"stroke":73,"style":1119},"170","28","6",[76,1729,1733],{"x":1730,"y":1731,"style":1732},"105","65","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:middle","92 – 100",[60,1735],{"x":96,"y":1736,"width":1737,"height":1738,"rx":1727,"fill":72,"stroke":73,"style":1119},"47","330","26",[60,1740],{"x":1741,"y":1742,"width":1743,"height":1744,"rx":1745,"fill":143,"stroke":144,"style":1119},"205","48","311.5","24","5",[76,1747,1750],{"x":1748,"y":1731,"style":1749},"546","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","accepted automatically",[60,1752],{"x":68,"y":1753,"width":1725,"height":1726,"rx":1727,"fill":65,"stroke":73,"style":1119},"90",[76,1755,1757],{"x":1730,"y":1756,"style":1732},"109","75 – 92",[60,1759],{"x":96,"y":1760,"width":1737,"height":1738,"rx":1727,"fill":72,"stroke":73,"style":1119},"91",[60,1762],{"x":1741,"y":89,"width":1763,"height":1744,"rx":1745,"fill":1154,"stroke":1155,"style":1119},"278.5",[76,1765,1767],{"x":1748,"y":1756,"style":1766},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","human review",[60,1769],{"x":68,"y":1770,"width":1725,"height":1726,"rx":1727,"fill":65,"stroke":73,"style":1119},"134",[76,1772,1774],{"x":1730,"y":1773,"style":1732},"153","below 75",[60,1776],{"x":96,"y":1777,"width":1737,"height":1738,"rx":1727,"fill":72,"stroke":73,"style":1119},"135",[60,1779],{"x":1741,"y":1780,"width":1781,"height":1744,"rx":1745,"fill":1782,"stroke":1783,"style":1119},"136","196","#fee8f2","var(--accent,#f43f8f)",[76,1785,1787],{"x":1748,"y":1773,"style":1786},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","no match",[76,1789,1792],{"x":1790,"y":1791,"style":160},"360","194","two thresholds beat one: certainty where it exists, a person where it does not",[163,1794,1796],{"id":1795},"step-5-score-everything-at-once-for-large-files","Step 5 — Score everything at once for large files",[10,1798,1799,1801,1802,1805],{},[14,1800,1699],{}," in a loop is fine for a few thousand names. For tens of thousands on each side, ",[14,1803,1804],{},"process.cdist"," computes the whole score matrix in compiled code, optionally\nacross all CPU cores, and NumPy then picks the best column per row.",[168,1807,1809],{"className":208,"code":1808,"language":210,"meta":173,"style":173},"import numpy as np\n\nscores = process.cdist(sales[\"key\"], finance[\"key\"], scorer=fuzz.WRatio, workers=-1)\nbest = scores.argmax(axis=1)\nsales[\"cdist_match\"] = finance[\"Account name\"].to_numpy()[best]\nsales[\"cdist_score\"] = scores[np.arange(len(sales)), best].astype(float).round(1)\nprint(sales[[\"Customer\", \"cdist_match\", \"cdist_score\"]])\n",[14,1810,1811,1823,1827,1863,1882,1900,1928],{"__ignoreMap":173},[177,1812,1813,1815,1818,1820],{"class":92,"line":179},[177,1814,218],{"class":217},[177,1816,1817],{"class":221}," numpy ",[177,1819,225],{"class":217},[177,1821,1822],{"class":221}," np\n",[177,1824,1825],{"class":92,"line":231},[177,1826,235],{"emptyLinePlaceholder":234},[177,1828,1829,1832,1834,1837,1839,1842,1844,1847,1849,1851,1853,1856,1859,1861],{"class":92,"line":238},[177,1830,1831],{"class":221},"scores ",[177,1833,343],{"class":217},[177,1835,1836],{"class":221}," process.cdist(sales[",[177,1838,725],{"class":186},[177,1840,1841],{"class":221},"], finance[",[177,1843,725],{"class":186},[177,1845,1846],{"class":221},"], ",[177,1848,1271],{"class":339},[177,1850,343],{"class":217},[177,1852,1276],{"class":221},[177,1854,1855],{"class":339},"workers",[177,1857,1858],{"class":217},"=-",[177,1860,1370],{"class":297},[177,1862,349],{"class":221},[177,1864,1865,1868,1870,1873,1876,1878,1880],{"class":92,"line":244},[177,1866,1867],{"class":221},"best ",[177,1869,343],{"class":217},[177,1871,1872],{"class":221}," scores.argmax(",[177,1874,1875],{"class":339},"axis",[177,1877,343],{"class":217},[177,1879,1370],{"class":297},[177,1881,349],{"class":221},[177,1883,1884,1886,1889,1891,1893,1895,1897],{"class":92,"line":275},[177,1885,722],{"class":221},[177,1887,1888],{"class":186},"\"cdist_match\"",[177,1890,728],{"class":221},[177,1892,343],{"class":217},[177,1894,753],{"class":221},[177,1896,756],{"class":186},[177,1898,1899],{"class":221},"].to_numpy()[best]\n",[177,1901,1902,1904,1907,1909,1911,1914,1916,1919,1921,1924,1926],{"class":92,"line":289},[177,1903,722],{"class":221},[177,1905,1906],{"class":186},"\"cdist_score\"",[177,1908,728],{"class":221},[177,1910,343],{"class":217},[177,1912,1913],{"class":221}," scores[np.arange(",[177,1915,827],{"class":297},[177,1917,1918],{"class":221},"(sales)), best].astype(",[177,1920,1250],{"class":297},[177,1922,1923],{"class":221},").round(",[177,1925,1370],{"class":297},[177,1927,349],{"class":221},[177,1929,1930,1932,1934,1936,1938,1940,1942,1944],{"class":92,"line":328},[177,1931,763],{"class":297},[177,1933,766],{"class":221},[177,1935,736],{"class":186},[177,1937,256],{"class":221},[177,1939,1888],{"class":186},[177,1941,256],{"class":221},[177,1943,1906],{"class":186},[177,1945,775],{"class":221},[10,1947,1948,1949,1952],{},"The matrix holds one score per pair, so memory grows with the product of the two lengths — 50,000 × 50,000 names is 2.5 billion scores. Beyond a few tens of thousands per\nside, split both files into blocks that can only match within themselves — same country, same postcode area, same first letter — and run ",[14,1950,1951],{},"cdist"," per block. Blocking also\nimproves accuracy, because it removes impossible candidates before they can score well by coincidence.",[163,1954,1956],{"id":1955},"step-6-export-a-review-sheet-people-can-work-through","Step 6 — Export a review sheet people can work through",[10,1958,1959],{},"The review band needs a human decision, and Excel is where that person works. Write the candidates side by side with the score and an empty column for the verdict, and read\nthe verdicts back on the next run.",[168,1961,1963],{"className":208,"code":1962,"language":210,"meta":173,"style":173},"review = joined.loc[joined[\"decision\"] == \"review\",\n                    [\"Customer\", \"Revenue\", \"Account name\", \"Credit limit\", \"score\"]].copy()\nreview[\"Same company? (Y\u002FN)\"] = \"\"\n\nwith pd.ExcelWriter(\"match_review.xlsx\", engine=\"openpyxl\") as writer:\n    joined.loc[joined[\"decision\"] == \"accepted\"].to_excel(writer, sheet_name=\"Accepted\", index=False)\n    review.to_excel(writer, sheet_name=\"Review\", index=False)\n    joined.loc[joined[\"decision\"] == \"no match\", [\"Customer\", \"Revenue\"]].to_excel(\n        writer, sheet_name=\"Unmatched\", index=False)\n    writer.sheets[\"Review\"].freeze_panes = \"A2\"\n\nprint(pd.ExcelFile(\"match_review.xlsx\").sheet_names)\n",[14,1964,1965,1987,2014,2029,2033,2062,2097,2119,2144,2166,2181,2185],{"__ignoreMap":173},[177,1966,1967,1970,1972,1975,1977,1979,1982,1985],{"class":92,"line":179},[177,1968,1969],{"class":221},"review ",[177,1971,343],{"class":217},[177,1973,1974],{"class":221}," joined.loc[joined[",[177,1976,1555],{"class":186},[177,1978,728],{"class":221},[177,1980,1981],{"class":217},"==",[177,1983,1984],{"class":186}," \"review\"",[177,1986,272],{"class":221},[177,1988,1989,1992,1994,1996,1999,2001,2003,2005,2007,2009,2011],{"class":92,"line":231},[177,1990,1991],{"class":221},"                    [",[177,1993,736],{"class":186},[177,1995,256],{"class":221},[177,1997,1998],{"class":186},"\"Revenue\"",[177,2000,256],{"class":221},[177,2002,756],{"class":186},[177,2004,256],{"class":221},[177,2006,1666],{"class":186},[177,2008,256],{"class":221},[177,2010,1320],{"class":186},[177,2012,2013],{"class":221},"]].copy()\n",[177,2015,2016,2019,2022,2024,2026],{"class":92,"line":238},[177,2017,2018],{"class":221},"review[",[177,2020,2021],{"class":186},"\"Same company? (Y\u002FN)\"",[177,2023,728],{"class":221},[177,2025,343],{"class":217},[177,2027,2028],{"class":186}," \"\"\n",[177,2030,2031],{"class":92,"line":244},[177,2032,235],{"emptyLinePlaceholder":234},[177,2034,2035,2038,2041,2044,2046,2049,2051,2054,2057,2059],{"class":92,"line":275},[177,2036,2037],{"class":217},"with",[177,2039,2040],{"class":221}," pd.ExcelWriter(",[177,2042,2043],{"class":186},"\"match_review.xlsx\"",[177,2045,256],{"class":221},[177,2047,2048],{"class":339},"engine",[177,2050,343],{"class":217},[177,2052,2053],{"class":186},"\"openpyxl\"",[177,2055,2056],{"class":221},") ",[177,2058,225],{"class":217},[177,2060,2061],{"class":221}," writer:\n",[177,2063,2064,2067,2069,2071,2073,2076,2079,2082,2084,2087,2089,2091,2093,2095],{"class":92,"line":289},[177,2065,2066],{"class":221},"    joined.loc[joined[",[177,2068,1555],{"class":186},[177,2070,728],{"class":221},[177,2072,1981],{"class":217},[177,2074,2075],{"class":186}," \"accepted\"",[177,2077,2078],{"class":221},"].to_excel(writer, ",[177,2080,2081],{"class":339},"sheet_name",[177,2083,343],{"class":217},[177,2085,2086],{"class":186},"\"Accepted\"",[177,2088,256],{"class":221},[177,2090,340],{"class":339},[177,2092,343],{"class":217},[177,2094,346],{"class":297},[177,2096,349],{"class":221},[177,2098,2099,2102,2104,2106,2109,2111,2113,2115,2117],{"class":92,"line":328},[177,2100,2101],{"class":221},"    review.to_excel(writer, ",[177,2103,2081],{"class":339},[177,2105,343],{"class":217},[177,2107,2108],{"class":186},"\"Review\"",[177,2110,256],{"class":221},[177,2112,340],{"class":339},[177,2114,343],{"class":217},[177,2116,346],{"class":297},[177,2118,349],{"class":221},[177,2120,2121,2123,2125,2127,2129,2132,2135,2137,2139,2141],{"class":92,"line":352},[177,2122,2066],{"class":221},[177,2124,1555],{"class":186},[177,2126,728],{"class":221},[177,2128,1981],{"class":217},[177,2130,2131],{"class":186}," \"no match\"",[177,2133,2134],{"class":221},", [",[177,2136,736],{"class":186},[177,2138,256],{"class":221},[177,2140,1998],{"class":186},[177,2142,2143],{"class":221},"]].to_excel(\n",[177,2145,2146,2149,2151,2153,2156,2158,2160,2162,2164],{"class":92,"line":357},[177,2147,2148],{"class":221},"        writer, ",[177,2150,2081],{"class":339},[177,2152,343],{"class":217},[177,2154,2155],{"class":186},"\"Unmatched\"",[177,2157,256],{"class":221},[177,2159,340],{"class":339},[177,2161,343],{"class":217},[177,2163,346],{"class":297},[177,2165,349],{"class":221},[177,2167,2168,2171,2173,2176,2178],{"class":92,"line":362},[177,2169,2170],{"class":221},"    writer.sheets[",[177,2172,2108],{"class":186},[177,2174,2175],{"class":221},"].freeze_panes ",[177,2177,343],{"class":217},[177,2179,2180],{"class":186}," \"A2\"\n",[177,2182,2183],{"class":92,"line":390},[177,2184,235],{"emptyLinePlaceholder":234},[177,2186,2187,2189,2192,2194],{"class":92,"line":403},[177,2188,763],{"class":297},[177,2190,2191],{"class":221},"(pd.ExcelFile(",[177,2193,2043],{"class":186},[177,2195,2196],{"class":221},").sheet_names)\n",[10,2198,2199,2200,2204],{},"Store confirmed pairs in a small mapping table and apply it before fuzzy matching on later runs, so each pair is only ever reviewed once. Over time the mapping becomes the\nshared ID the two systems never had. Formatting the review sheet for easier reading is covered in\n",[31,2201,2203],{"href":2202},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fwrite-a-formatted-excel-report-with-xlsxwriter\u002F","Write a Formatted Excel Report with xlsxwriter",".",[163,2206,2208],{"id":2207},"common-pitfalls","Common pitfalls",[2210,2211,2212,2228],"table",{},[2213,2214,2215],"thead",{},[2216,2217,2218,2222,2225],"tr",{},[2219,2220,2221],"th",{},"Symptom",[2219,2223,2224],{},"Cause",[2219,2226,2227],{},"Fix",[2229,2230,2231,2246,2263,2274,2285,2298],"tbody",{},[2216,2232,2233,2237,2240],{},[2234,2235,2236],"td",{},"Every row gets a match, many wrong",[2234,2238,2239],{},"No score cut-off",[2234,2241,2242,2243,2245],{},"Use ",[14,2244,1189],{}," and a no-match band",[2216,2247,2248,2251,2256],{},[2234,2249,2250],{},"Short names match unrelated long ones",[2234,2252,2253,2255],{},[14,2254,906],{}," treats subsets as perfect",[2234,2257,2242,2258,884,2260,2262],{},[14,2259,910],{},[14,2261,902],{},"; require minimum length",[2216,2264,2265,2268,2271],{},[2234,2266,2267],{},"Obvious matches score low",[2234,2269,2270],{},"Suffixes and punctuation not removed",[2234,2272,2273],{},"Normalise before scoring",[2216,2275,2276,2279,2282],{},[2234,2277,2278],{},"Two companies merged into one",[2234,2280,2281],{},"Normalisation removed a distinguishing word",[2234,2283,2284],{},"Shorten the suffix list; review merged keys",[2216,2286,2287,2290,2295],{},[2234,2288,2289],{},"Memory error on large files",[2234,2291,2292,2294],{},[14,2293,1951],{}," matrix too big",[2234,2296,2297],{},"Block by country, postcode or first letter",[2216,2299,2300,2303,2306],{},[2234,2301,2302],{},"Results change between runs",[2234,2304,2305],{},"Ties between equally scored candidates",[2234,2307,2308],{},"Break ties explicitly, e.g. by a second field",[163,2310,2312],{"id":2311},"performance-and-scale-notes","Performance and scale notes",[10,2314,2315,2316,2318,2319,856,2321,2324,2325,2328],{},"rapidfuzz is implemented in C++ and is dramatically faster than pure-Python matching; ",[14,2317,1699],{}," handles thousands of queries per second against lists of thousands of\nchoices. ",[14,2320,1951],{},[14,2322,2323],{},"workers=-1"," is the fastest option for all-pairs scoring but is bounded by memory, which blocking solves. Normalising once into a key column, rather\nthan inside the scorer through a ",[14,2326,2327],{},"processor",", avoids repeating the same regex work for every pair.",[163,2330,2332],{"id":2331},"conclusion","Conclusion",[10,2334,2335,2336,2338,2339,2341],{},"Normalise names first — case, punctuation and legal suffixes — because it turns many fuzzy problems into exact ones. Pick a scorer by testing known pairs, use ",[14,2337,1699],{},"\nwith a cut-off so unmatched names stay unmatched, and split decisions into accepted, review and no-match bands. Scale with ",[14,2340,1951],{}," and blocking, write the review band to a sheet\npeople can work through, and keep confirmed pairs as a mapping so each decision is made only once.",[163,2343,2345],{"id":2344},"frequently-asked-questions","Frequently asked questions",[10,2347,2348,2352],{},[2349,2350,2351],"strong",{},"Is rapidfuzz the same as fuzzywuzzy?","\nIt provides the same scorers with a compatible API, but it is implemented in C++, much faster, and MIT-licensed. For new code rapidfuzz is the better choice.",[10,2354,2355,2358],{},[2349,2356,2357],{},"What score threshold should I use?","\nThere is no universal number. Score a sample of pairs you already know are matches and non-matches, and choose thresholds that separate them; for company names, accepting from the low nineties and reviewing from the mid seventies is a reasonable starting point.",[10,2360,2361,2364],{},[2349,2362,2363],{},"Can I fuzzy match on more than one column?","\nYes. Score names and addresses separately and combine the scores, or block on an exact field such as postcode and fuzzy match names only within each block.",[10,2366,2367,2370],{},[2349,2368,2369],{},"Can Excel do fuzzy matching itself?","\nPower Query has a fuzzy merge option with a similarity threshold. Python gives you more control over normalisation, scorers and review workflows, and runs unattended.",[163,2372,2374],{"id":2373},"related","Related",[2376,2377,2378,2385,2392,2399,2406],"ul",{},[2379,2380,2381,2382,2384],"li",{},"Up one level: ",[31,2383,34],{"href":33}," — every way to combine spreadsheet data.",[2379,2386,2387,2391],{},[31,2388,2390],{"href":2389},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-two-excel-files-on-common-column-python\u002F","Merge Two Excel Files on a Common Column in Python"," — the exact-match join this page extends.",[2379,2393,2394,2398],{},[31,2395,2397],{"href":2396},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffind-rows-in-one-excel-file-missing-from-another\u002F","Find Rows in One Excel File Missing from Another"," — reporting the unmatched rows.",[2379,2400,2401,2405],{},[31,2402,2404],{"href":2403},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fstrip-whitespace-and-normalise-text-columns-with-pandas\u002F","Strip Whitespace and Normalise Text Columns with pandas"," — deeper text normalisation.",[2379,2407,2408,2412],{},[31,2409,2411],{"href":2410},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-duplicate-rows-in-excel-with-python\u002F","Find Duplicate Rows in Excel with Python"," — near-duplicates within a single file.",[2414,2415,2416],"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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":173,"searchDepth":231,"depth":231,"links":2418},[2419,2420,2421,2422,2423,2424,2425,2426,2427,2428,2429,2430],{"id":165,"depth":231,"text":166},{"id":493,"depth":231,"text":494},{"id":891,"depth":231,"text":892},{"id":1179,"depth":231,"text":1180},{"id":1443,"depth":231,"text":1444},{"id":1795,"depth":231,"text":1796},{"id":1955,"depth":231,"text":1956},{"id":2207,"depth":231,"text":2208},{"id":2311,"depth":231,"text":2312},{"id":2331,"depth":231,"text":2332},{"id":2344,"depth":231,"text":2345},{"id":2373,"depth":231,"text":2374},"2026-09-17","Join two spreadsheets when the names do not quite match — Acme Ltd vs ACME Limited — with rapidfuzz: normalise names, choose a scorer, set a cut-off, score every pair at once, and send doubtful matches for review.","md",[2435,2437,2439,2441],{"q":2351,"a":2436},"It provides the same scorers with a compatible API, but it is implemented in C++, much faster, and MIT-licensed. For new code rapidfuzz is the better choice.",{"q":2357,"a":2438},"There is no universal number. Score a sample of pairs you already know are matches and non-matches, and choose thresholds that separate them; for company names, accepting from the low nineties and reviewing from the mid seventies is a reasonable starting point.",{"q":2363,"a":2440},"Yes. Score names and addresses separately and combine the scores, or block on an exact field such as postcode and fuzzy match names only within each block.",{"q":2369,"a":2442},"Power Query has a fuzzy merge option with a similarity threshold. Python gives you more control over normalisation, scorers and review workflows, and runs unattended.","fuzzy match names between two excel files with python",null,{"breadcrumb":2446},[2447,2450,2453],{"name":2448,"item":2449},"Home","\u002F",{"name":2451,"item":2452},"Advanced Data Transformation and Cleaning","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":34,"item":33},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffuzzy-match-names-between-two-excel-files-with-python",{"title":2456,"description":2457},"Fuzzy Match Names Between Two Excel Files with Python and rapidfuzz","Fuzzy join Excel data in Python: normalise company and customer names, compare rapidfuzz scorers, use extractOne and cdist with score cut-offs, block large files, and export a review sheet.","fuzzy-match-names-between-two-excel-files-with-python","advanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffuzzy-match-names-between-two-excel-files-with-python\u002Findex",[2461,2462,2463],"joins-and-lookups","data-cleaning","pandas","how-to","Q5CGLn9p1LShLm3q_U-asbkY_KC-PPIK5vr985-Y4TI",[2467,2470],{"title":2397,"path":2468,"stem":2469,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffind-rows-in-one-excel-file-missing-from-another","advanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffind-rows-in-one-excel-file-missing-from-another\u002Findex",{"title":2471,"path":2472,"stem":2473,"children":-1},"Handle Many-to-Many Lookups Without Duplicating Rows","\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fhandle-many-to-many-lookups-without-duplicating-rows","advanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fhandle-many-to-many-lookups-without-duplicating-rows\u002Findex",[2475,2478],{"path":2476,"title":2477},"\u002Fadvanced-data-transformation-and-cleaning","Advanced Data Transformation and Cleaning for Excel Automation",{"path":2479,"title":2480},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes","Merging and Joining Excel DataFrames with Pandas",[],[2483,2486,2489,2492,2495,2498],{"path":2484,"title":2485,"kind":2464},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcompare-two-excel-files-for-differences-with-python","Compare Two Excel Files for Differences with Python",{"path":2487,"title":2488,"kind":2464},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table","Convert Multi-Currency Transactions with an FX Rate Table",{"path":2490,"title":2491,"kind":2464},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-differences-between-two-columns-with-openpyxl","Highlight Differences Between Two Columns with openpyxl",{"path":2493,"title":2494,"kind":2464},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fmap-free-text-job-titles-to-standard-categories","Map Free-Text Job Titles to Standard Categories",{"path":2496,"title":2497,"kind":2464},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fstandardise-category-values-with-a-mapping-table","Standardise Category Values with a Mapping Table in pandas",{"path":2499,"title":2500,"kind":2464},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fapproximate-match-vlookup-in-pandas","Approximate-Match VLOOKUP for Tiered Rates in pandas",1790972096240]