[{"data":1,"prerenderedAt":2280},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-equivalent-in-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-equivalent-in-pandas":2246,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-equivalent-in-pandas":2255,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-equivalent-in-pandas":2260,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-equivalent-in-pandas":2261},{"id":4,"title":5,"body":6,"dateModified":2212,"datePublished":2212,"description":2213,"extension":2214,"faq":2215,"keyword":2224,"kind":2225,"meta":2226,"navigation":227,"path":2235,"seo":2236,"slug":2239,"stem":2240,"topics":2241,"type":2244,"__hash__":2245},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-equivalent-in-pandas\u002Findex.md","XLOOKUP Equivalent in pandas",{"type":7,"value":8,"toc":2197},"minimark",[9,37,169,174,202,205,525,529,538,628,631,635,638,719,734,738,745,831,845,1010,1014,1033,1255,1269,1273,1291,1418,1440,1444,1451,1588,1598,1602,1605,1885,1888,1892,1898,2042,2045,2049,2107,2111,2134,2138,2144,2150,2156,2162,2166,2193],[10,11,12,13,18,19,23,24,27,28,31,32,36],"p",{},"XLOOKUP replaced VLOOKUP and INDEX\u002FMATCH for many Excel users because one function covers exact and approximate matches, a default for missing keys, and a choice between first and last match. When the workbook logic moves to Python, each of those arguments needs a pandas counterpart. This guide, part of ",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002F","Excel Formula Equivalents in pandas",", goes through them one at a time and ends with an ",[20,21,22],"code",{},"xlookup"," function you can drop into a project. The general lookup patterns — ",[20,25,26],{},"map",", ",[20,29,30],{},"merge",", composite keys, two-dimensional lookups — are covered in ",[14,33,35],{"href":34},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Findex-match-equivalent-in-pandas\u002F","INDEX MATCH Equivalent in pandas","; this page focuses on what is specific to XLOOKUP.",[38,39,47,48,47,52,47,56,47,64,47,72,47,79,47,86,47,91,47,97,47,99,47,104,47,107,47,111,47,113,47,117,47,120,47,124,47,126,47,130,47,133,47,137,47,139,47,143,47,146,47,150,47,152,47,156,47,159,47,163,47,165],"svg",{"viewBox":40,"role":41,"ariaLabelledBy":42,"xmlns":45,"style":46},"4 0 752 276","img",[43,44],"xl-args-t","xl-args-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[49,50,51],"title",{"id":43},"Each XLOOKUP argument and its pandas counterpart",[53,54,55],"desc",{"id":44},"lookup_value and lookup_array become the key column and a lookup Series index. return_array becomes the mapped values or merged columns. if_not_found becomes fillna. match_mode 0 is map, -1 and 1 are merge_asof backward and forward, and 2 is a wildcard match. search_mode 1 and -1 become drop_duplicates keeping first or last.",[57,58],"rect",{"x":59,"y":60,"width":61,"height":62,"fill":63},"4","0","752","276","#ffffff",[57,65],{"x":66,"y":67,"width":68,"height":69,"rx":60,"fill":63,"stroke":70,"style":71},"20.0","16.0","204.3","40.0","var(--line,#cdd5e6)","stroke-width:1px",[57,73],{"x":74,"y":67,"width":75,"height":69,"rx":60,"fill":76,"stroke":77,"style":78},"224.3","515.7","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.2px",[80,81,85],"text",{"x":82,"y":83,"style":84},"482.1","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","pandas",[57,87],{"x":66,"y":88,"width":68,"height":89,"rx":60,"fill":90,"stroke":70,"style":71},"56.0","34.0","#e7ebef",[80,92,96],{"x":93,"y":94,"style":95},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","lookup_value, lookup_array",[57,98],{"x":74,"y":88,"width":75,"height":89,"rx":60,"fill":63,"stroke":70,"style":71},[80,100,103],{"x":82,"y":101,"style":102},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","key column, lookup index",[57,105],{"x":66,"y":106,"width":68,"height":89,"rx":60,"fill":90,"stroke":70,"style":71},"90.0",[80,108,110],{"x":93,"y":109,"style":95},"112.0","return_array",[57,112],{"x":74,"y":106,"width":75,"height":89,"rx":60,"fill":63,"stroke":70,"style":71},[80,114,116],{"x":82,"y":115,"style":102},"111.0","map() or merge()",[57,118],{"x":66,"y":119,"width":68,"height":89,"rx":60,"fill":90,"stroke":70,"style":71},"124.0",[80,121,123],{"x":93,"y":122,"style":95},"146.0","if_not_found",[57,125],{"x":74,"y":119,"width":75,"height":89,"rx":60,"fill":63,"stroke":70,"style":71},[80,127,129],{"x":82,"y":128,"style":102},"145.0","fillna(default)",[57,131],{"x":66,"y":132,"width":68,"height":89,"rx":60,"fill":90,"stroke":70,"style":71},"158.0",[80,134,136],{"x":93,"y":135,"style":95},"180.0","match_mode 0 \u002F -1 \u002F 1",[57,138],{"x":74,"y":132,"width":75,"height":89,"rx":60,"fill":63,"stroke":70,"style":71},[80,140,142],{"x":82,"y":141,"style":102},"179.0","map \u002F merge_asof back \u002F fwd",[57,144],{"x":66,"y":145,"width":68,"height":89,"rx":60,"fill":90,"stroke":70,"style":71},"192.0",[80,147,149],{"x":93,"y":148,"style":95},"214.0","match_mode 2 (wildcards)",[57,151],{"x":74,"y":145,"width":75,"height":89,"rx":60,"fill":63,"stroke":70,"style":71},[80,153,155],{"x":82,"y":154,"style":102},"213.0","fnmatch or regex",[57,157],{"x":66,"y":158,"width":68,"height":89,"rx":60,"fill":90,"stroke":70,"style":71},"226.0",[80,160,162],{"x":93,"y":161,"style":95},"248.0","search_mode 1 \u002F -1",[57,164],{"x":74,"y":158,"width":75,"height":89,"rx":60,"fill":63,"stroke":70,"style":71},[80,166,168],{"x":82,"y":167,"style":102},"247.0","keep first \u002F keep last",[170,171,173],"h2",{"id":172},"prerequisites","Prerequisites",[175,176,181],"pre",{"className":177,"code":178,"language":179,"meta":180,"style":180},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[20,182,183],{"__ignoreMap":180},[184,185,188,192,196,199],"span",{"class":186,"line":187},"line",1,[184,189,191],{"class":190},"sMTad","pip",[184,193,195],{"class":194},"srMev"," install",[184,197,198],{"class":194}," pandas",[184,200,201],{"class":194}," openpyxl\n",[10,203,204],{},"A price list with a duplicated key and inconsistent case, and orders to price:",[175,206,210],{"className":207,"code":208,"language":209,"meta":180,"style":180},"language-python shiki shiki-themes github-light github-dark-high-contrast","import fnmatch\n\nimport pandas as pd\n\nprices = pd.DataFrame({\n    \"sku\": [\"A-100\", \"a-200\", \"B-300\", \"A-100\"],\n    \"price\": [9.5, 12.0, 30.0, 9.9],\n    \"supplier\": [\"North\", \"North\", \"South\", \"West\"],\n})\norders = pd.DataFrame({\"order\": [1, 2, 3, 4], \"sku\": [\"A-100\", \"A-200\", \"C-999\", \"B-300\"]})\nwith pd.ExcelWriter(\"lookup.xlsx\") as writer:\n    prices.to_excel(writer, sheet_name=\"Prices\", index=False)\n    orders.to_excel(writer, sheet_name=\"Orders\", index=False)\nprices = pd.read_excel(\"lookup.xlsx\", sheet_name=\"Prices\")\norders = pd.read_excel(\"lookup.xlsx\", sheet_name=\"Orders\")\n","python",[20,211,212,222,229,243,248,260,289,318,345,351,411,431,459,482,504],{"__ignoreMap":180},[184,213,214,218],{"class":186,"line":187},[184,215,217],{"class":216},"s-kum","import",[184,219,221],{"class":220},"skGVy"," fnmatch\n",[184,223,225],{"class":186,"line":224},2,[184,226,228],{"emptyLinePlaceholder":227},true,"\n",[184,230,232,234,237,240],{"class":186,"line":231},3,[184,233,217],{"class":216},[184,235,236],{"class":220}," pandas ",[184,238,239],{"class":216},"as",[184,241,242],{"class":220}," pd\n",[184,244,246],{"class":186,"line":245},4,[184,247,228],{"emptyLinePlaceholder":227},[184,249,251,254,257],{"class":186,"line":250},5,[184,252,253],{"class":220},"prices ",[184,255,256],{"class":216},"=",[184,258,259],{"class":220}," pd.DataFrame({\n",[184,261,263,266,269,272,274,277,279,282,284,286],{"class":186,"line":262},6,[184,264,265],{"class":194},"    \"sku\"",[184,267,268],{"class":220},": [",[184,270,271],{"class":194},"\"A-100\"",[184,273,27],{"class":220},[184,275,276],{"class":194},"\"a-200\"",[184,278,27],{"class":220},[184,280,281],{"class":194},"\"B-300\"",[184,283,27],{"class":220},[184,285,271],{"class":194},[184,287,288],{"class":220},"],\n",[184,290,292,295,297,301,303,306,308,311,313,316],{"class":186,"line":291},7,[184,293,294],{"class":194},"    \"price\"",[184,296,268],{"class":220},[184,298,300],{"class":299},"sP0c6","9.5",[184,302,27],{"class":220},[184,304,305],{"class":299},"12.0",[184,307,27],{"class":220},[184,309,310],{"class":299},"30.0",[184,312,27],{"class":220},[184,314,315],{"class":299},"9.9",[184,317,288],{"class":220},[184,319,321,324,326,329,331,333,335,338,340,343],{"class":186,"line":320},8,[184,322,323],{"class":194},"    \"supplier\"",[184,325,268],{"class":220},[184,327,328],{"class":194},"\"North\"",[184,330,27],{"class":220},[184,332,328],{"class":194},[184,334,27],{"class":220},[184,336,337],{"class":194},"\"South\"",[184,339,27],{"class":220},[184,341,342],{"class":194},"\"West\"",[184,344,288],{"class":220},[184,346,348],{"class":186,"line":347},9,[184,349,350],{"class":220},"})\n",[184,352,354,357,359,362,365,367,370,372,375,377,380,382,384,387,390,392,394,396,399,401,404,406,408],{"class":186,"line":353},10,[184,355,356],{"class":220},"orders ",[184,358,256],{"class":216},[184,360,361],{"class":220}," pd.DataFrame({",[184,363,364],{"class":194},"\"order\"",[184,366,268],{"class":220},[184,368,369],{"class":299},"1",[184,371,27],{"class":220},[184,373,374],{"class":299},"2",[184,376,27],{"class":220},[184,378,379],{"class":299},"3",[184,381,27],{"class":220},[184,383,59],{"class":299},[184,385,386],{"class":220},"], ",[184,388,389],{"class":194},"\"sku\"",[184,391,268],{"class":220},[184,393,271],{"class":194},[184,395,27],{"class":220},[184,397,398],{"class":194},"\"A-200\"",[184,400,27],{"class":220},[184,402,403],{"class":194},"\"C-999\"",[184,405,27],{"class":220},[184,407,281],{"class":194},[184,409,410],{"class":220},"]})\n",[184,412,414,417,420,423,426,428],{"class":186,"line":413},11,[184,415,416],{"class":216},"with",[184,418,419],{"class":220}," pd.ExcelWriter(",[184,421,422],{"class":194},"\"lookup.xlsx\"",[184,424,425],{"class":220},") ",[184,427,239],{"class":216},[184,429,430],{"class":220}," writer:\n",[184,432,434,437,441,443,446,448,451,453,456],{"class":186,"line":433},12,[184,435,436],{"class":220},"    prices.to_excel(writer, ",[184,438,440],{"class":439},"sa561","sheet_name",[184,442,256],{"class":216},[184,444,445],{"class":194},"\"Prices\"",[184,447,27],{"class":220},[184,449,450],{"class":439},"index",[184,452,256],{"class":216},[184,454,455],{"class":299},"False",[184,457,458],{"class":220},")\n",[184,460,462,465,467,469,472,474,476,478,480],{"class":186,"line":461},13,[184,463,464],{"class":220},"    orders.to_excel(writer, ",[184,466,440],{"class":439},[184,468,256],{"class":216},[184,470,471],{"class":194},"\"Orders\"",[184,473,27],{"class":220},[184,475,450],{"class":439},[184,477,256],{"class":216},[184,479,455],{"class":299},[184,481,458],{"class":220},[184,483,485,487,489,492,494,496,498,500,502],{"class":186,"line":484},14,[184,486,253],{"class":220},[184,488,256],{"class":216},[184,490,491],{"class":220}," pd.read_excel(",[184,493,422],{"class":194},[184,495,27],{"class":220},[184,497,440],{"class":439},[184,499,256],{"class":216},[184,501,445],{"class":194},[184,503,458],{"class":220},[184,505,507,509,511,513,515,517,519,521,523],{"class":186,"line":506},15,[184,508,356],{"class":220},[184,510,256],{"class":216},[184,512,491],{"class":220},[184,514,422],{"class":194},[184,516,27],{"class":220},[184,518,440],{"class":439},[184,520,256],{"class":216},[184,522,471],{"class":194},[184,524,458],{"class":220},[170,526,528],{"id":527},"step-1-exact-match-with-if_not_found","Step 1 — Exact match with if_not_found",[10,530,531,534,535,537],{},[20,532,533],{},"=XLOOKUP(B2, Prices!A:A, Prices!B:B, 0)"," returns the price or 0 when the SKU is not listed. In pandas, index the price column by SKU, ",[20,536,26],{}," the order SKUs through it and fill the gaps:",[175,539,541],{"className":207,"code":540,"language":209,"meta":180,"style":180},"lookup = prices.drop_duplicates(\"sku\").set_index(\"sku\")[\"price\"]\norders[\"price\"] = orders[\"sku\"].map(lookup)\nprint(orders[\"price\"].tolist())                  # [9.5, nan, nan, 30.0]\norders[\"price_or_0\"] = orders[\"price\"].fillna(0)\n",[20,542,543,569,589,606],{"__ignoreMap":180},[184,544,545,548,550,553,555,558,560,563,566],{"class":186,"line":187},[184,546,547],{"class":220},"lookup ",[184,549,256],{"class":216},[184,551,552],{"class":220}," prices.drop_duplicates(",[184,554,389],{"class":194},[184,556,557],{"class":220},").set_index(",[184,559,389],{"class":194},[184,561,562],{"class":220},")[",[184,564,565],{"class":194},"\"price\"",[184,567,568],{"class":220},"]\n",[184,570,571,574,576,579,581,584,586],{"class":186,"line":224},[184,572,573],{"class":220},"orders[",[184,575,565],{"class":194},[184,577,578],{"class":220},"] ",[184,580,256],{"class":216},[184,582,583],{"class":220}," orders[",[184,585,389],{"class":194},[184,587,588],{"class":220},"].map(lookup)\n",[184,590,591,594,597,599,602],{"class":186,"line":231},[184,592,593],{"class":299},"print",[184,595,596],{"class":220},"(orders[",[184,598,565],{"class":194},[184,600,601],{"class":220},"].tolist())                  ",[184,603,605],{"class":604},"s-wDw","# [9.5, nan, nan, 30.0]\n",[184,607,608,610,613,615,617,619,621,624,626],{"class":186,"line":245},[184,609,573],{"class":220},[184,611,612],{"class":194},"\"price_or_0\"",[184,614,578],{"class":220},[184,616,256],{"class":216},[184,618,583],{"class":220},[184,620,565],{"class":194},[184,622,623],{"class":220},"].fillna(",[184,625,60],{"class":299},[184,627,458],{"class":220},[10,629,630],{},"Keep the unfilled column until you have reported the misses: a default of 0 makes an unknown SKU look like a free item. Two orders missed here — C-999, which is genuinely absent, and A-200, which is in the list as \"a-200\". Excel would have found that one.",[170,632,634],{"id":633},"step-2-excels-case-insensitive-matching","Step 2 — Excel's case-insensitive matching",[10,636,637],{},"XLOOKUP ignores case: \"A-200\" matches \"a-200\". pandas compares strings exactly. To get Excel's result, casefold the key on both sides:",[175,639,641],{"className":207,"code":640,"language":209,"meta":180,"style":180},"lookup = (prices.assign(key=prices[\"sku\"].str.casefold())\n                .drop_duplicates(\"key\")\n                .set_index(\"key\")[\"price\"])\norders[\"price\"] = orders[\"sku\"].str.casefold().map(lookup)\nprint(orders[\"price\"].tolist())                  # [9.5, 12.0, nan, 30.0]\n",[20,642,643,665,675,689,706],{"__ignoreMap":180},[184,644,645,647,649,652,655,657,660,662],{"class":186,"line":187},[184,646,547],{"class":220},[184,648,256],{"class":216},[184,650,651],{"class":220}," (prices.assign(",[184,653,654],{"class":439},"key",[184,656,256],{"class":216},[184,658,659],{"class":220},"prices[",[184,661,389],{"class":194},[184,663,664],{"class":220},"].str.casefold())\n",[184,666,667,670,673],{"class":186,"line":224},[184,668,669],{"class":220},"                .drop_duplicates(",[184,671,672],{"class":194},"\"key\"",[184,674,458],{"class":220},[184,676,677,680,682,684,686],{"class":186,"line":231},[184,678,679],{"class":220},"                .set_index(",[184,681,672],{"class":194},[184,683,562],{"class":220},[184,685,565],{"class":194},[184,687,688],{"class":220},"])\n",[184,690,691,693,695,697,699,701,703],{"class":186,"line":245},[184,692,573],{"class":220},[184,694,565],{"class":194},[184,696,578],{"class":220},[184,698,256],{"class":216},[184,700,583],{"class":220},[184,702,389],{"class":194},[184,704,705],{"class":220},"].str.casefold().map(lookup)\n",[184,707,708,710,712,714,716],{"class":186,"line":250},[184,709,593],{"class":299},[184,711,596],{"class":220},[184,713,565],{"class":194},[184,715,601],{"class":220},[184,717,718],{"class":604},"# [9.5, 12.0, nan, 30.0]\n",[10,720,721,724,725,728,729,733],{},[20,722,723],{},"casefold"," is a stronger form of ",[20,726,727],{},"lower"," that also handles characters such as the German \"ß\". If the price list has trailing spaces or non-breaking spaces as well — common in hand-maintained sheets — clean those too; see ",[14,730,732],{"href":731},"\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",".",[170,735,737],{"id":736},"step-3-first-or-last-match-with-search_mode","Step 3 — First or last match with search_mode",[10,739,740,741,744],{},"A-100 appears twice in the price list, at 9.5 and 9.9. XLOOKUP's default ",[20,742,743],{},"search_mode"," of 1 returns the first; -1 searches from the bottom and returns the last — the usual choice when newer rows are appended below older ones. In pandas this is simply which duplicate you keep:",[175,746,748],{"className":207,"code":747,"language":209,"meta":180,"style":180},"first = prices.drop_duplicates(\"sku\", keep=\"first\").set_index(\"sku\")[\"price\"]\nlast = prices.drop_duplicates(\"sku\", keep=\"last\").set_index(\"sku\")[\"price\"]\nprint(first[\"A-100\"], last[\"A-100\"])             # 9.5 9.9\n",[20,749,750,781,811],{"__ignoreMap":180},[184,751,752,755,757,759,761,763,766,768,771,773,775,777,779],{"class":186,"line":187},[184,753,754],{"class":220},"first ",[184,756,256],{"class":216},[184,758,552],{"class":220},[184,760,389],{"class":194},[184,762,27],{"class":220},[184,764,765],{"class":439},"keep",[184,767,256],{"class":216},[184,769,770],{"class":194},"\"first\"",[184,772,557],{"class":220},[184,774,389],{"class":194},[184,776,562],{"class":220},[184,778,565],{"class":194},[184,780,568],{"class":220},[184,782,783,786,788,790,792,794,796,798,801,803,805,807,809],{"class":186,"line":224},[184,784,785],{"class":220},"last ",[184,787,256],{"class":216},[184,789,552],{"class":220},[184,791,389],{"class":194},[184,793,27],{"class":220},[184,795,765],{"class":439},[184,797,256],{"class":216},[184,799,800],{"class":194},"\"last\"",[184,802,557],{"class":220},[184,804,389],{"class":194},[184,806,562],{"class":220},[184,808,565],{"class":194},[184,810,568],{"class":220},[184,812,813,815,818,820,823,825,828],{"class":186,"line":231},[184,814,593],{"class":299},[184,816,817],{"class":220},"(first[",[184,819,271],{"class":194},[184,821,822],{"class":220},"], last[",[184,824,271],{"class":194},[184,826,827],{"class":220},"])             ",[184,829,830],{"class":604},"# 9.5 9.9\n",[10,832,833,834,837,838,840,841,844],{},"Do not skip this step: ",[20,835,836],{},"set_index"," on a column with duplicates, followed by ",[20,839,26],{},", raises ",[20,842,843],{},"InvalidIndexError: Reindexing only valid with uniquely valued Index objects",". Whether duplicates should exist at all is a question for the owner of the price list; count them and report them before choosing one.",[38,846,47,852,47,855,47,858,47,863,47,867,47,871,47,877,47,881,47,885,47,889,47,893,47,896,47,900,47,902,47,906,47,908,47,911,47,913,47,916,47,919,47,922,47,925,47,929,47,931,47,934,47,936,47,939,47,942,47,945,47,947,47,951,47,953,47,956,47,958,47,960,47,962,47,965,47,967,47,970,47,972,47,975,47,977,47,980,47,983,47,987,47,991,47,994,47,996,47,999,47,1001,47,1004],{"viewBox":847,"role":41,"ariaLabelledBy":848,"xmlns":45,"style":851},"92 0 576 222",[849,850],"xl-dupes-t","xl-dupes-d","width:100%;max-width:576px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[49,853,854],{"id":849},"A duplicated key, and which row each search_mode returns",[53,856,857],{"id":850},"The price list holds A-100 twice, first at 9.50 from supplier North and later at 9.90 from supplier West. search_mode 1, the default, returns the first row. search_mode -1 returns the last. In pandas the choice is drop_duplicates with keep first or keep last.",[57,859],{"x":860,"y":60,"width":861,"height":862,"fill":63},"92","576","222",[57,864],{"x":865,"y":67,"width":93,"height":866,"rx":60,"fill":90,"stroke":70,"style":71},"264.3","24.0",[57,868],{"x":869,"y":67,"width":870,"height":866,"rx":60,"fill":90,"stroke":70,"style":71},"296.3","63.4",[80,872,876],{"x":873,"y":874,"style":875},"328.0","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[57,878],{"x":879,"y":67,"width":880,"height":866,"rx":60,"fill":90,"stroke":70,"style":71},"359.6","61.8",[80,882,884],{"x":883,"y":874,"style":875},"390.6","B",[57,886],{"x":887,"y":67,"width":888,"height":866,"rx":60,"fill":90,"stroke":70,"style":71},"421.5","74.2",[80,890,892],{"x":891,"y":874,"style":875},"458.6","C",[57,894],{"x":865,"y":69,"width":93,"height":895,"rx":60,"fill":90,"stroke":70,"style":71},"28.0",[80,897,369],{"x":898,"y":899,"style":875},"280.3","59.0",[57,901],{"x":869,"y":69,"width":870,"height":895,"rx":60,"fill":63,"stroke":70,"style":71},[80,903,905],{"x":873,"y":899,"style":904},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","sku",[57,907],{"x":879,"y":69,"width":880,"height":895,"rx":60,"fill":63,"stroke":70,"style":71},[80,909,910],{"x":883,"y":899,"style":904},"price",[57,912],{"x":887,"y":69,"width":888,"height":895,"rx":60,"fill":63,"stroke":70,"style":71},[80,914,915],{"x":891,"y":899,"style":904},"supplier",[57,917],{"x":865,"y":918,"width":93,"height":895,"rx":60,"fill":90,"stroke":70,"style":71},"68.0",[80,920,374],{"x":898,"y":921,"style":875},"87.0",[57,923],{"x":869,"y":918,"width":870,"height":895,"rx":60,"fill":76,"stroke":77,"style":924},"stroke-width:1.5px",[80,926,928],{"x":873,"y":921,"style":927},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","A-100",[57,930],{"x":879,"y":918,"width":880,"height":895,"rx":60,"fill":76,"stroke":77,"style":924},[80,932,933],{"x":883,"y":921,"style":927},"9.50",[57,935],{"x":887,"y":918,"width":888,"height":895,"rx":60,"fill":76,"stroke":77,"style":924},[80,937,938],{"x":891,"y":921,"style":927},"North",[57,940],{"x":865,"y":941,"width":93,"height":895,"rx":60,"fill":90,"stroke":70,"style":71},"96.0",[80,943,379],{"x":898,"y":944,"style":875},"115.0",[57,946],{"x":869,"y":941,"width":870,"height":895,"rx":60,"fill":63,"stroke":70,"style":71},[80,948,950],{"x":873,"y":944,"style":949},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","a-200",[57,952],{"x":879,"y":941,"width":880,"height":895,"rx":60,"fill":63,"stroke":70,"style":71},[80,954,955],{"x":883,"y":944,"style":949},"12.00",[57,957],{"x":887,"y":941,"width":888,"height":895,"rx":60,"fill":63,"stroke":70,"style":71},[80,959,938],{"x":891,"y":944,"style":949},[57,961],{"x":865,"y":119,"width":93,"height":895,"rx":60,"fill":90,"stroke":70,"style":71},[80,963,59],{"x":898,"y":964,"style":875},"143.0",[57,966],{"x":869,"y":119,"width":870,"height":895,"rx":60,"fill":63,"stroke":70,"style":71},[80,968,969],{"x":873,"y":964,"style":949},"B-300",[57,971],{"x":879,"y":119,"width":880,"height":895,"rx":60,"fill":63,"stroke":70,"style":71},[80,973,974],{"x":883,"y":964,"style":949},"30.00",[57,976],{"x":887,"y":119,"width":888,"height":895,"rx":60,"fill":63,"stroke":70,"style":71},[80,978,979],{"x":891,"y":964,"style":949},"South",[57,981],{"x":865,"y":982,"width":93,"height":895,"rx":60,"fill":90,"stroke":70,"style":71},"152.0",[80,984,986],{"x":898,"y":985,"style":875},"171.0","5",[57,988],{"x":869,"y":982,"width":870,"height":895,"rx":60,"fill":989,"stroke":990,"style":924},"#fdefd8","var(--gold,#b4740a)",[80,992,928],{"x":873,"y":985,"style":993},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[57,995],{"x":879,"y":982,"width":880,"height":895,"rx":60,"fill":989,"stroke":990,"style":924},[80,997,998],{"x":883,"y":985,"style":993},"9.90",[57,1000],{"x":887,"y":982,"width":888,"height":895,"rx":60,"fill":989,"stroke":990,"style":924},[80,1002,1003],{"x":891,"y":985,"style":993},"West",[80,1005,1009],{"x":1006,"y":1007,"style":1008},"380.0","202.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","blue is returned by search_mode 1 (keep=\"first\"), gold by search_mode -1 (keep=\"last\")",[170,1011,1013],{"id":1012},"step-4-next-smaller-or-next-larger-with-match_mode","Step 4 — Next smaller or next larger with match_mode",[10,1015,1016,1017,1020,1021,1024,1025,1028,1029,1032],{},"Discount tiers, tax bands and shipping rates are looked up with ",[20,1018,1019],{},"match_mode"," -1 (exact match or the next smaller item) or 1 (exact match or the next larger item). ",[20,1022,1023],{},"merge_asof"," does both, with ",[20,1026,1027],{},"direction=\"backward\""," and ",[20,1030,1031],{},"\"forward\"",":",[175,1034,1036],{"className":207,"code":1035,"language":209,"meta":180,"style":180},"tiers = pd.DataFrame({\"min_qty\": [1, 10, 50, 100], \"discount\": [0.0, 0.05, 0.10, 0.15]})\nqty = pd.DataFrame({\"line\": [1, 2, 3, 4, 5], \"qty\": [75, 5, 250, 10, 0]})\n\nmatched = pd.merge_asof(qty.sort_values(\"qty\"), tiers,\n                        left_on=\"qty\", right_on=\"min_qty\", direction=\"backward\")\nmatched = matched.sort_values(\"line\").reset_index(drop=True)\nprint(matched[[\"line\", \"qty\", \"discount\"]].values.tolist())\n# [[1.0, 75.0, 0.1], [2.0, 5.0, 0.0], [3.0, 250.0, 0.15], [4.0, 10.0, 0.05], [5.0, 0.0, nan]]\n",[20,1037,1038,1096,1157,1161,1176,1206,1230,1250],{"__ignoreMap":180},[184,1039,1040,1043,1045,1047,1050,1052,1054,1056,1059,1061,1064,1066,1069,1071,1074,1076,1079,1081,1084,1086,1089,1091,1094],{"class":186,"line":187},[184,1041,1042],{"class":220},"tiers ",[184,1044,256],{"class":216},[184,1046,361],{"class":220},[184,1048,1049],{"class":194},"\"min_qty\"",[184,1051,268],{"class":220},[184,1053,369],{"class":299},[184,1055,27],{"class":220},[184,1057,1058],{"class":299},"10",[184,1060,27],{"class":220},[184,1062,1063],{"class":299},"50",[184,1065,27],{"class":220},[184,1067,1068],{"class":299},"100",[184,1070,386],{"class":220},[184,1072,1073],{"class":194},"\"discount\"",[184,1075,268],{"class":220},[184,1077,1078],{"class":299},"0.0",[184,1080,27],{"class":220},[184,1082,1083],{"class":299},"0.05",[184,1085,27],{"class":220},[184,1087,1088],{"class":299},"0.10",[184,1090,27],{"class":220},[184,1092,1093],{"class":299},"0.15",[184,1095,410],{"class":220},[184,1097,1098,1101,1103,1105,1108,1110,1112,1114,1116,1118,1120,1122,1124,1126,1128,1130,1133,1135,1138,1140,1142,1144,1147,1149,1151,1153,1155],{"class":186,"line":224},[184,1099,1100],{"class":220},"qty ",[184,1102,256],{"class":216},[184,1104,361],{"class":220},[184,1106,1107],{"class":194},"\"line\"",[184,1109,268],{"class":220},[184,1111,369],{"class":299},[184,1113,27],{"class":220},[184,1115,374],{"class":299},[184,1117,27],{"class":220},[184,1119,379],{"class":299},[184,1121,27],{"class":220},[184,1123,59],{"class":299},[184,1125,27],{"class":220},[184,1127,986],{"class":299},[184,1129,386],{"class":220},[184,1131,1132],{"class":194},"\"qty\"",[184,1134,268],{"class":220},[184,1136,1137],{"class":299},"75",[184,1139,27],{"class":220},[184,1141,986],{"class":299},[184,1143,27],{"class":220},[184,1145,1146],{"class":299},"250",[184,1148,27],{"class":220},[184,1150,1058],{"class":299},[184,1152,27],{"class":220},[184,1154,60],{"class":299},[184,1156,410],{"class":220},[184,1158,1159],{"class":186,"line":231},[184,1160,228],{"emptyLinePlaceholder":227},[184,1162,1163,1166,1168,1171,1173],{"class":186,"line":245},[184,1164,1165],{"class":220},"matched ",[184,1167,256],{"class":216},[184,1169,1170],{"class":220}," pd.merge_asof(qty.sort_values(",[184,1172,1132],{"class":194},[184,1174,1175],{"class":220},"), tiers,\n",[184,1177,1178,1181,1183,1185,1187,1190,1192,1194,1196,1199,1201,1204],{"class":186,"line":250},[184,1179,1180],{"class":439},"                        left_on",[184,1182,256],{"class":216},[184,1184,1132],{"class":194},[184,1186,27],{"class":220},[184,1188,1189],{"class":439},"right_on",[184,1191,256],{"class":216},[184,1193,1049],{"class":194},[184,1195,27],{"class":220},[184,1197,1198],{"class":439},"direction",[184,1200,256],{"class":216},[184,1202,1203],{"class":194},"\"backward\"",[184,1205,458],{"class":220},[184,1207,1208,1210,1212,1215,1217,1220,1223,1225,1228],{"class":186,"line":262},[184,1209,1165],{"class":220},[184,1211,256],{"class":216},[184,1213,1214],{"class":220}," matched.sort_values(",[184,1216,1107],{"class":194},[184,1218,1219],{"class":220},").reset_index(",[184,1221,1222],{"class":439},"drop",[184,1224,256],{"class":216},[184,1226,1227],{"class":299},"True",[184,1229,458],{"class":220},[184,1231,1232,1234,1237,1239,1241,1243,1245,1247],{"class":186,"line":291},[184,1233,593],{"class":299},[184,1235,1236],{"class":220},"(matched[[",[184,1238,1107],{"class":194},[184,1240,27],{"class":220},[184,1242,1132],{"class":194},[184,1244,27],{"class":220},[184,1246,1073],{"class":194},[184,1248,1249],{"class":220},"]].values.tolist())\n",[184,1251,1252],{"class":186,"line":320},[184,1253,1254],{"class":604},"# [[1.0, 75.0, 0.1], [2.0, 5.0, 0.0], [3.0, 250.0, 0.15], [4.0, 10.0, 0.05], [5.0, 0.0, nan]]\n",[10,1256,1257,1258,1261,1262,1265,1266,1268],{},"Both sides must be sorted by the key, so sort the left table, merge, and sort back by an order column to restore the original row order. A quantity of 0 is below the first tier, so it has no next smaller item — Excel returns ",[20,1259,1260],{},"#N\u002FA"," there, and pandas returns ",[20,1263,1264],{},"NaN",". XLOOKUP does not need the lookup column sorted for these modes, but ",[20,1267,1023],{}," does, which is why the tier table is kept in ascending order.",[170,1270,1272],{"id":1271},"step-5-wildcards-with-match_mode-2","Step 5 — Wildcards with match_mode 2",[10,1274,1275,1276,1278,1279,1282,1283,1286,1287,1290],{},"With ",[20,1277,1019],{}," 2, XLOOKUP accepts ",[20,1280,1281],{},"*"," for any characters and ",[20,1284,1285],{},"?"," for one character. Python's ",[20,1288,1289],{},"fnmatch"," uses the same symbols. Casefold both sides to keep Excel's case-insensitivity:",[175,1292,1294],{"className":207,"code":1293,"language":209,"meta":180,"style":180},"def wildcard_first(pattern, keys, values):\n    pat = pattern.casefold()\n    for key, value in zip(keys, values):\n        if fnmatch.fnmatchcase(str(key).casefold(), pat):\n            return value\n    return None\n\nprint(wildcard_first(\"b*\", prices[\"sku\"], prices[\"price\"]))       # 30.0\nprint(wildcard_first(\"A-?00\", prices[\"sku\"], prices[\"price\"]))    # 9.5\n",[20,1295,1296,1308,1318,1335,1349,1357,1365,1369,1395],{"__ignoreMap":180},[184,1297,1298,1301,1305],{"class":186,"line":187},[184,1299,1300],{"class":216},"def",[184,1302,1304],{"class":1303},"s_Opv"," wildcard_first",[184,1306,1307],{"class":220},"(pattern, keys, values):\n",[184,1309,1310,1313,1315],{"class":186,"line":224},[184,1311,1312],{"class":220},"    pat ",[184,1314,256],{"class":216},[184,1316,1317],{"class":220}," pattern.casefold()\n",[184,1319,1320,1323,1326,1329,1332],{"class":186,"line":231},[184,1321,1322],{"class":216},"    for",[184,1324,1325],{"class":220}," key, value ",[184,1327,1328],{"class":216},"in",[184,1330,1331],{"class":299}," zip",[184,1333,1334],{"class":220},"(keys, values):\n",[184,1336,1337,1340,1343,1346],{"class":186,"line":245},[184,1338,1339],{"class":216},"        if",[184,1341,1342],{"class":220}," fnmatch.fnmatchcase(",[184,1344,1345],{"class":299},"str",[184,1347,1348],{"class":220},"(key).casefold(), pat):\n",[184,1350,1351,1354],{"class":186,"line":250},[184,1352,1353],{"class":216},"            return",[184,1355,1356],{"class":220}," value\n",[184,1358,1359,1362],{"class":186,"line":262},[184,1360,1361],{"class":216},"    return",[184,1363,1364],{"class":299}," None\n",[184,1366,1367],{"class":186,"line":291},[184,1368,228],{"emptyLinePlaceholder":227},[184,1370,1371,1373,1376,1379,1382,1384,1387,1389,1392],{"class":186,"line":320},[184,1372,593],{"class":299},[184,1374,1375],{"class":220},"(wildcard_first(",[184,1377,1378],{"class":194},"\"b*\"",[184,1380,1381],{"class":220},", prices[",[184,1383,389],{"class":194},[184,1385,1386],{"class":220},"], prices[",[184,1388,565],{"class":194},[184,1390,1391],{"class":220},"]))       ",[184,1393,1394],{"class":604},"# 30.0\n",[184,1396,1397,1399,1401,1404,1406,1408,1410,1412,1415],{"class":186,"line":347},[184,1398,593],{"class":299},[184,1400,1375],{"class":220},[184,1402,1403],{"class":194},"\"A-?00\"",[184,1405,1381],{"class":220},[184,1407,389],{"class":194},[184,1409,1386],{"class":220},[184,1411,565],{"class":194},[184,1413,1414],{"class":220},"]))    ",[184,1416,1417],{"class":604},"# 9.5\n",[10,1419,1420,1421,1424,1425,1427,1428,1431,1432,1435,1436,1439],{},"Excel escapes a literal asterisk as ",[20,1422,1423],{},"~*","; ",[20,1426,1289],{}," uses ",[20,1429,1430],{},"[*]"," instead. Wildcard lookups scan the list row by row, which is fine for a few hundred patterns. For many thousands, translate the patterns to regular expressions and use ",[20,1433,1434],{},"str.contains"," or ",[20,1437,1438],{},"str.match"," on the whole column.",[170,1441,1443],{"id":1442},"step-6-return-several-columns-at-once","Step 6 — Return several columns at once",[10,1445,1446,1447,1450],{},"XLOOKUP can return a whole row — ",[20,1448,1449],{},"=XLOOKUP(B2, Prices!A:A, Prices!B:C)"," spills price and supplier. In pandas that is a left merge on the prepared key, selecting the columns to bring across:",[175,1452,1454],{"className":207,"code":1453,"language":209,"meta":180,"style":180},"table = (prices.assign(key=prices[\"sku\"].str.casefold())\n               .drop_duplicates(\"key\", keep=\"last\")[[\"key\", \"price\", \"supplier\"]])\nresult = orders[[\"order\", \"sku\"]].assign(key=orders[\"sku\"].str.casefold()) \\\n    .merge(table, on=\"key\", how=\"left\", validate=\"many_to_one\").drop(columns=\"key\")\nprint(result.values.tolist())\n",[20,1455,1456,1475,1507,1537,1581],{"__ignoreMap":180},[184,1457,1458,1461,1463,1465,1467,1469,1471,1473],{"class":186,"line":187},[184,1459,1460],{"class":220},"table ",[184,1462,256],{"class":216},[184,1464,651],{"class":220},[184,1466,654],{"class":439},[184,1468,256],{"class":216},[184,1470,659],{"class":220},[184,1472,389],{"class":194},[184,1474,664],{"class":220},[184,1476,1477,1480,1482,1484,1486,1488,1490,1493,1495,1497,1499,1501,1504],{"class":186,"line":224},[184,1478,1479],{"class":220},"               .drop_duplicates(",[184,1481,672],{"class":194},[184,1483,27],{"class":220},[184,1485,765],{"class":439},[184,1487,256],{"class":216},[184,1489,800],{"class":194},[184,1491,1492],{"class":220},")[[",[184,1494,672],{"class":194},[184,1496,27],{"class":220},[184,1498,565],{"class":194},[184,1500,27],{"class":220},[184,1502,1503],{"class":194},"\"supplier\"",[184,1505,1506],{"class":220},"]])\n",[184,1508,1509,1512,1514,1517,1519,1521,1523,1526,1528,1530,1532,1534],{"class":186,"line":231},[184,1510,1511],{"class":220},"result ",[184,1513,256],{"class":216},[184,1515,1516],{"class":220}," orders[[",[184,1518,364],{"class":194},[184,1520,27],{"class":220},[184,1522,389],{"class":194},[184,1524,1525],{"class":220},"]].assign(",[184,1527,654],{"class":439},[184,1529,256],{"class":216},[184,1531,573],{"class":220},[184,1533,389],{"class":194},[184,1535,1536],{"class":220},"].str.casefold()) \\\n",[184,1538,1539,1542,1545,1547,1549,1551,1554,1556,1559,1561,1564,1566,1569,1572,1575,1577,1579],{"class":186,"line":245},[184,1540,1541],{"class":220},"    .merge(table, ",[184,1543,1544],{"class":439},"on",[184,1546,256],{"class":216},[184,1548,672],{"class":194},[184,1550,27],{"class":220},[184,1552,1553],{"class":439},"how",[184,1555,256],{"class":216},[184,1557,1558],{"class":194},"\"left\"",[184,1560,27],{"class":220},[184,1562,1563],{"class":439},"validate",[184,1565,256],{"class":216},[184,1567,1568],{"class":194},"\"many_to_one\"",[184,1570,1571],{"class":220},").drop(",[184,1573,1574],{"class":439},"columns",[184,1576,256],{"class":216},[184,1578,672],{"class":194},[184,1580,458],{"class":220},[184,1582,1583,1585],{"class":186,"line":250},[184,1584,593],{"class":299},[184,1586,1587],{"class":220},"(result.values.tolist())\n",[10,1589,1590,1593,1594,733],{},[20,1591,1592],{},"validate=\"many_to_one\""," makes pandas raise an error if the lookup side still has duplicate keys, which would otherwise multiply order rows silently. Merge validation is covered in more depth in ",[14,1595,1597],{"href":1596},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fvalidate-a-merge-to-avoid-duplicate-rows\u002F","Validate a Merge to Avoid Duplicate Rows",[170,1599,1601],{"id":1600},"a-reusable-xlookup-function","A reusable xlookup function",[10,1603,1604],{},"Put the pieces together so a translated workbook reads close to the original formulas:",[175,1606,1608],{"className":207,"code":1607,"language":209,"meta":180,"style":180},"def xlookup(values, keys, returns, if_not_found=None, search_mode=1, ignore_case=True):\n    keys = pd.Series(keys).reset_index(drop=True)\n    returns = pd.Series(returns).reset_index(drop=True)\n    values = pd.Series(values)\n    norm = (lambda s: s.astype(str).str.casefold()) if ignore_case else (lambda s: s)\n    table = pd.DataFrame({\"k\": norm(keys), \"r\": returns})\n    table = table.drop_duplicates(\"k\", keep=\"first\" if search_mode == 1 else \"last\")\n    out = norm(values).map(table.set_index(\"k\")[\"r\"])\n    return out if if_not_found is None else out.fillna(if_not_found)\n\norders[\"price\"] = xlookup(orders[\"sku\"], prices[\"sku\"], prices[\"price\"], if_not_found=0, search_mode=-1)\nprint(orders[\"price\"].tolist())                  # [9.9, 12.0, 0.0, 30.0]\n",[20,1609,1610,1642,1660,1678,1688,1725,1746,1785,1803,1826,1830,1872],{"__ignoreMap":180},[184,1611,1612,1614,1617,1620,1622,1625,1628,1630,1632,1635,1637,1639],{"class":186,"line":187},[184,1613,1300],{"class":216},[184,1615,1616],{"class":1303}," xlookup",[184,1618,1619],{"class":220},"(values, keys, returns, if_not_found",[184,1621,256],{"class":216},[184,1623,1624],{"class":299},"None",[184,1626,1627],{"class":220},", search_mode",[184,1629,256],{"class":216},[184,1631,369],{"class":299},[184,1633,1634],{"class":220},", ignore_case",[184,1636,256],{"class":216},[184,1638,1227],{"class":299},[184,1640,1641],{"class":220},"):\n",[184,1643,1644,1647,1649,1652,1654,1656,1658],{"class":186,"line":224},[184,1645,1646],{"class":220},"    keys ",[184,1648,256],{"class":216},[184,1650,1651],{"class":220}," pd.Series(keys).reset_index(",[184,1653,1222],{"class":439},[184,1655,256],{"class":216},[184,1657,1227],{"class":299},[184,1659,458],{"class":220},[184,1661,1662,1665,1667,1670,1672,1674,1676],{"class":186,"line":231},[184,1663,1664],{"class":220},"    returns ",[184,1666,256],{"class":216},[184,1668,1669],{"class":220}," pd.Series(returns).reset_index(",[184,1671,1222],{"class":439},[184,1673,256],{"class":216},[184,1675,1227],{"class":299},[184,1677,458],{"class":220},[184,1679,1680,1683,1685],{"class":186,"line":245},[184,1681,1682],{"class":220},"    values ",[184,1684,256],{"class":216},[184,1686,1687],{"class":220}," pd.Series(values)\n",[184,1689,1690,1693,1695,1698,1701,1704,1706,1709,1712,1715,1718,1720,1722],{"class":186,"line":250},[184,1691,1692],{"class":220},"    norm ",[184,1694,256],{"class":216},[184,1696,1697],{"class":220}," (",[184,1699,1700],{"class":216},"lambda",[184,1702,1703],{"class":220}," s: s.astype(",[184,1705,1345],{"class":299},[184,1707,1708],{"class":220},").str.casefold()) ",[184,1710,1711],{"class":216},"if",[184,1713,1714],{"class":220}," ignore_case ",[184,1716,1717],{"class":216},"else",[184,1719,1697],{"class":220},[184,1721,1700],{"class":216},[184,1723,1724],{"class":220}," s: s)\n",[184,1726,1727,1730,1732,1734,1737,1740,1743],{"class":186,"line":262},[184,1728,1729],{"class":220},"    table ",[184,1731,256],{"class":216},[184,1733,361],{"class":220},[184,1735,1736],{"class":194},"\"k\"",[184,1738,1739],{"class":220},": norm(keys), ",[184,1741,1742],{"class":194},"\"r\"",[184,1744,1745],{"class":220},": returns})\n",[184,1747,1748,1750,1752,1755,1757,1759,1761,1763,1765,1768,1771,1774,1777,1780,1783],{"class":186,"line":291},[184,1749,1729],{"class":220},[184,1751,256],{"class":216},[184,1753,1754],{"class":220}," table.drop_duplicates(",[184,1756,1736],{"class":194},[184,1758,27],{"class":220},[184,1760,765],{"class":439},[184,1762,256],{"class":216},[184,1764,770],{"class":194},[184,1766,1767],{"class":216}," if",[184,1769,1770],{"class":220}," search_mode ",[184,1772,1773],{"class":216},"==",[184,1775,1776],{"class":299}," 1",[184,1778,1779],{"class":216}," else",[184,1781,1782],{"class":194}," \"last\"",[184,1784,458],{"class":220},[184,1786,1787,1790,1792,1795,1797,1799,1801],{"class":186,"line":320},[184,1788,1789],{"class":220},"    out ",[184,1791,256],{"class":216},[184,1793,1794],{"class":220}," norm(values).map(table.set_index(",[184,1796,1736],{"class":194},[184,1798,562],{"class":220},[184,1800,1742],{"class":194},[184,1802,688],{"class":220},[184,1804,1805,1807,1810,1812,1815,1818,1821,1823],{"class":186,"line":347},[184,1806,1361],{"class":216},[184,1808,1809],{"class":220}," out ",[184,1811,1711],{"class":216},[184,1813,1814],{"class":220}," if_not_found ",[184,1816,1817],{"class":216},"is",[184,1819,1820],{"class":299}," None",[184,1822,1779],{"class":216},[184,1824,1825],{"class":220}," out.fillna(if_not_found)\n",[184,1827,1828],{"class":186,"line":353},[184,1829,228],{"emptyLinePlaceholder":227},[184,1831,1832,1834,1836,1838,1840,1843,1845,1847,1849,1851,1853,1855,1857,1859,1861,1863,1865,1868,1870],{"class":186,"line":413},[184,1833,573],{"class":220},[184,1835,565],{"class":194},[184,1837,578],{"class":220},[184,1839,256],{"class":216},[184,1841,1842],{"class":220}," xlookup(orders[",[184,1844,389],{"class":194},[184,1846,1386],{"class":220},[184,1848,389],{"class":194},[184,1850,1386],{"class":220},[184,1852,565],{"class":194},[184,1854,386],{"class":220},[184,1856,123],{"class":439},[184,1858,256],{"class":216},[184,1860,60],{"class":299},[184,1862,27],{"class":220},[184,1864,743],{"class":439},[184,1866,1867],{"class":216},"=-",[184,1869,369],{"class":299},[184,1871,458],{"class":220},[184,1873,1874,1876,1878,1880,1882],{"class":186,"line":433},[184,1875,593],{"class":299},[184,1877,596],{"class":220},[184,1879,565],{"class":194},[184,1881,601],{"class":220},[184,1883,1884],{"class":604},"# [9.9, 12.0, 0.0, 30.0]\n",[10,1886,1887],{},"The defaults match Excel's: case-insensitive, first match, no default. Because every argument is named, a reviewer comparing the script with the old workbook can check each translated formula argument by argument. Approximate and wildcard modes stay separate functions, because they need sorted input and row-by-row matching respectively.",[170,1889,1891],{"id":1890},"why-not-translate-the-formula-row-by-row","Why not translate the formula row by row?",[10,1893,1894,1895,1897],{},"The most literal translation of a column full of XLOOKUP formulas is a function applied to every row, each one scanning the price list for its SKU. It gives the right answer and is very slow. On a test with 200,000 order lines and 20,000 SKUs, ",[20,1896,26],{}," finished in under a tenth of a second, while a row-by-row lambda that filtered the price list for each order took about 40 seconds. Excel hides this cost because it recalculates cells incrementally; a script pays it every run.",[38,1899,47,1904,47,1907,47,1910,47,1913,47,1920,47,1931,47,1935,47,1939,47,1943,47,1946,47,1951,47,1957,47,1962,47,1966,47,1969,47,1973,47,1976,47,1979,47,1983,47,1986,47,1991,47,1994,47,1997,47,2000,47,2003,47,2007,47,2010,47,2013,47,2016,47,2019,47,2021,47,2024,47,2028,47,2031,47,2036,47,2039],{"viewBox":1900,"role":41,"ariaLabelledBy":1901,"xmlns":45,"style":46},"4 0 752 238",[1902,1903],"xl-choose-t","xl-choose-d",[49,1905,1906],{"id":1902},"Which pandas tool to use for an XLOOKUP",[53,1908,1909],{"id":1903},"For an exact match returning one column, use map on a unique lookup Series. For several returned columns use a validated left merge. For next smaller or larger matches use merge_asof on sorted keys. For wildcard patterns use fnmatch or a regular expression.",[57,1911],{"x":59,"y":60,"width":61,"height":1912,"fill":63},"238",[57,1914],{"x":1915,"y":67,"width":1916,"height":1917,"rx":1918,"fill":76,"stroke":77,"style":1919},"240.0","280.0","75.5","12","stroke-width:2px",[1921,1922,1924],"g",{"transform":1923},"translate(368.0,29.0) scale(1.000)",[1925,1926],"path",{"d":1927,"fill":1928,"stroke":1929,"style":1930},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[80,1932,1934],{"x":1006,"y":1917,"style":1933},"font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","What kind of XLOOKUP is it?",[186,1936],{"x1":1006,"y1":1937,"x2":1006,"y2":1938,"stroke":77,"style":1919},"91.5","111.5",[186,1940],{"x1":1941,"y1":1938,"x2":1942,"y2":1938,"stroke":77,"style":1919},"104.0","656.0",[186,1944],{"x1":1941,"y1":1938,"x2":1941,"y2":1945,"stroke":77,"style":1919},"157.5",[1947,1948],"polygon",{"points":1949,"fill":1950},"104.0,161.5 99.0,152.5 109.0,152.5","#5b5cf0",[57,1952],{"x":1953,"y":1954,"width":1955,"height":1956,"rx":1058,"fill":63,"stroke":70,"style":78},"40.8","121.5","126.5","21.0",[80,1958,1961],{"x":1941,"y":1959,"style":1960},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","exact, one column",[57,1963],{"x":66,"y":1964,"width":1965,"height":899,"rx":1918,"fill":76,"stroke":77,"style":1919},"163.5","168.0",[80,1967,26],{"x":1941,"y":1968,"style":1933},"189.0",[80,1970,1972],{"x":1941,"y":1971,"style":102},"208.5","plus fillna",[186,1974],{"x1":1975,"y1":1938,"x2":1975,"y2":1945,"stroke":77,"style":1919},"288.0",[1947,1977],{"points":1978,"fill":1950},"288.0,161.5 283.0,152.5 293.0,152.5",[57,1980],{"x":1981,"y":1954,"width":1982,"height":1956,"rx":1058,"fill":63,"stroke":70,"style":78},"210.6","154.7",[80,1984,1985],{"x":1975,"y":1959,"style":1960},"exact, several columns",[57,1987],{"x":1988,"y":1964,"width":1965,"height":899,"rx":1918,"fill":1989,"stroke":1990,"style":1919},"204.0","#d9f4f1","var(--teal,#0f9488)",[80,1992,30],{"x":1975,"y":1968,"style":1993},"font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[80,1995,1996],{"x":1975,"y":1971,"style":102},"validate=many_to_one",[186,1998],{"x1":1999,"y1":1938,"x2":1999,"y2":1945,"stroke":77,"style":1919},"472.0",[1947,2001],{"points":2002,"fill":1950},"472.0,161.5 467.0,152.5 477.0,152.5",[57,2004],{"x":2005,"y":1954,"width":2006,"height":1956,"rx":1058,"fill":63,"stroke":70,"style":78},"406.5","131.0",[80,2008,2009],{"x":1999,"y":1959,"style":1960},"match_mode -1 or 1",[57,2011],{"x":2012,"y":1964,"width":1965,"height":899,"rx":1918,"fill":989,"stroke":990,"style":1919},"388.0",[80,2014,1023],{"x":1999,"y":1968,"style":2015},"font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[80,2017,2018],{"x":1999,"y":1971,"style":102},"sorted keys",[186,2020],{"x1":1942,"y1":1938,"x2":1942,"y2":1945,"stroke":77,"style":1919},[1947,2022],{"points":2023,"fill":1950},"656.0,161.5 651.0,152.5 661.0,152.5",[57,2025],{"x":2026,"y":1954,"width":2027,"height":1956,"rx":1058,"fill":63,"stroke":70,"style":78},"603.7","104.5",[80,2029,2030],{"x":1942,"y":1959,"style":1960},"match_mode 2",[57,2032],{"x":2033,"y":1964,"width":1965,"height":899,"rx":1918,"fill":2034,"stroke":2035,"style":1919},"572.0","#fee8f2","var(--accent,#f43f8f)",[80,2037,1289],{"x":1942,"y":1968,"style":2038},"font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[80,2040,2041],{"x":1942,"y":1971,"style":102},"row by row",[10,2043,2044],{},"Build the lookup Series or table once, outside any loop, and apply it to the whole column. That also makes the lookup table a single object you can inspect, test and report on, instead of logic spread across thousands of cells.",[170,2046,2048],{"id":2047},"common-pitfalls","Common pitfalls",[2050,2051,2052,2067,2079,2088,2098],"ul",{},[2053,2054,2055,2059,2060,2062,2063,2066],"li",{},[2056,2057,2058],"strong",{},"Numbers stored as text."," A key of ",[20,2061,1068],{}," never matches ",[20,2064,2065],{},"\"100\"",", in Excel or in pandas. Convert both sides to one type first.",[2053,2068,2069,2072,2073,2075,2076,2078],{},[2056,2070,2071],{},"Duplicate keys in the lookup table."," ",[20,2074,26],{}," refuses them and ",[20,2077,30],{}," multiplies rows. Choose first or last explicitly.",[2053,2080,2081,2072,2084,2087],{},[2056,2082,2083],{},"Whole-column references in the original.",[20,2085,2086],{},"Prices!A:A"," in Excel includes the header and blank rows. When you translate, read the table with a proper header and drop empty keys, or a blank lookup value can match a blank row.",[2053,2089,2090,2093,2094,2097],{},[2056,2091,2092],{},"Dates as keys."," A date cell read as a timestamp never equals the text \"2026-01-05\". Convert both sides with ",[20,2095,2096],{},"pd.to_datetime"," first.",[2053,2099,2100,2072,2103,2106],{},[2056,2101,2102],{},"A default that hides errors.",[20,2104,2105],{},"if_not_found=0"," turns missing prices into free items. Report misses before filling.",[170,2108,2110],{"id":2109},"conclusion","Conclusion",[10,2112,2113,2114,2116,2117,2120,2121,2124,2125,2127,2128,2130,2131,2133],{},"XLOOKUP's arguments map cleanly onto pandas: ",[20,2115,26],{}," with ",[20,2118,2119],{},"fillna"," for exact lookups with a default, casefolded keys for Excel's case-insensitivity, ",[20,2122,2123],{},"drop_duplicates(keep=…)"," for search mode, ",[20,2126,1023],{}," for next smaller or larger, ",[20,2129,1289],{}," for wildcards and a validated left merge to return several columns. Wrap the common case in one ",[20,2132,22],{}," function and keep the unusual modes explicit.",[170,2135,2137],{"id":2136},"frequently-asked-questions","Frequently asked questions",[10,2139,2140,2143],{},[2056,2141,2142],{},"What is the pandas equivalent of XLOOKUP?","\nFor a single returned column, Series.map with a lookup Series indexed by the key; for several returned columns, DataFrame.merge with how=\"left\". Approximate matches use merge_asof.",[10,2145,2146,2149],{},[2056,2147,2148],{},"Is XLOOKUP case-sensitive?","\nNo. XLOOKUP treats \"a-200\" and \"A-200\" as the same key, while pandas comparisons are case-sensitive. Casefold both sides of the lookup to reproduce Excel's behaviour.",[10,2151,2152,2155],{},[2056,2153,2154],{},"How do I return the last match instead of the first?","\nDrop duplicate keys from the lookup table with keep=\"last\" before mapping. That matches XLOOKUP with search_mode -1, which searches from the bottom up.",[10,2157,2158,2161],{},[2056,2159,2160],{},"What replaces the if_not_found argument?","\nCall fillna on the result with the default value. Keep the unfilled result as well if you need to report which keys were missing.",[170,2163,2165],{"id":2164},"related","Related",[2050,2167,2168,2174,2179,2186],{},[2053,2169,2170,2171,2173],{},"Up one level: ",[14,2172,17],{"href":16}," — Excel functions translated to pandas.",[2053,2175,2176,2178],{},[14,2177,35],{"href":34}," — the general lookup patterns.",[2053,2180,2181,2185],{},[14,2182,2184],{"href":2183},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fsumif-and-sumifs-equivalent-in-pandas\u002F","SUMIF and SUMIFS Equivalent in pandas"," — conditional totals.",[2053,2187,2188,2192],{},[14,2189,2191],{"href":2190},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmatch-rows-by-nearest-date-with-merge-asof\u002F","Match Rows by Nearest Date with merge_asof"," — approximate matching on dates.",[2194,2195,2196],"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-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":180,"searchDepth":224,"depth":224,"links":2198},[2199,2200,2201,2202,2203,2204,2205,2206,2207,2208,2209,2210,2211],{"id":172,"depth":224,"text":173},{"id":527,"depth":224,"text":528},{"id":633,"depth":224,"text":634},{"id":736,"depth":224,"text":737},{"id":1012,"depth":224,"text":1013},{"id":1271,"depth":224,"text":1272},{"id":1442,"depth":224,"text":1443},{"id":1600,"depth":224,"text":1601},{"id":1890,"depth":224,"text":1891},{"id":2047,"depth":224,"text":2048},{"id":2109,"depth":224,"text":2110},{"id":2136,"depth":224,"text":2137},{"id":2164,"depth":224,"text":2165},"2026-09-23","Reproduce every XLOOKUP argument in pandas — exact lookups with a default for missing keys, Excel's case-insensitive matching, first or last match, next smaller or next larger item, wildcards, and returning several columns at once — with a reusable xlookup function.","md",[2216,2218,2220,2222],{"q":2142,"a":2217},"For a single returned column, Series.map with a lookup Series indexed by the key; for several returned columns, DataFrame.merge with how=\"left\". Approximate matches use merge_asof.",{"q":2148,"a":2219},"No. XLOOKUP treats \"a-200\" and \"A-200\" as the same key, while pandas comparisons are case-sensitive. Casefold both sides of the lookup to reproduce Excel's behaviour.",{"q":2154,"a":2221},"Drop duplicate keys from the lookup table with keep=\"last\" before mapping. That matches XLOOKUP with search_mode -1, which searches from the bottom up.",{"q":2160,"a":2223},"Call fillna on the result with the default value. Keep the unfilled result as well if you need to report which keys were missing.","xlookup equivalent in pandas",null,{"breadcrumb":2227},[2228,2231,2234],{"name":2229,"item":2230},"Home","\u002F",{"name":2232,"item":2233},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":17,"item":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-equivalent-in-pandas",{"title":2237,"description":2238},"XLOOKUP Equivalent in pandas (if_not_found, match_mode, search_mode)","Translate Excel XLOOKUP to pandas — map with a default for if_not_found, case-insensitive keys, first versus last match, next smaller or larger with merge_asof, wildcard matches and multi-column returns.","xlookup-equivalent-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-equivalent-in-pandas\u002Findex",[2242,2243,85],"formula-equivalents","joins-and-lookups","how-to","W8BCdq0hFcf6Vi3IQCXIIOcfzsFYdP8VhWqxEkwUuJY",[2247,2251],{"title":2248,"path":2249,"stem":2250,"children":-1},"XIRR and XNPV Equivalents in Python","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxirr-and-xnpv-equivalents-in-python","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxirr-and-xnpv-equivalents-in-python\u002Findex",{"title":2252,"path":2253,"stem":2254,"children":-1},"XLOOKUP with Multiple Criteria in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-with-multiple-criteria-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-with-multiple-criteria-in-pandas\u002Findex",[2256,2258],{"path":2257,"title":17},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas",{"path":2259,"title":2232},"\u002Fadvanced-data-transformation-and-cleaning",[],[2262,2265,2268,2271,2274,2277],{"path":2263,"title":2264,"kind":2244},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fvlookup-equivalent-in-pandas-for-excel-files","The VLOOKUP Equivalent in pandas for Excel Files",{"path":2266,"title":2267,"kind":2244},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fextract-numbers-and-codes-from-text-with-pandas","Extract Numbers and Codes from Text with pandas",{"path":2269,"title":2270,"kind":2244},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fsplit-full-names-into-first-and-last-names-with-pandas","Split Full Names into First and Last Names with pandas",{"path":2272,"title":2273,"kind":2244},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbin-values-into-ranges-with-pandas-cut","Bin Values into Ranges Like Excel FREQUENCY with pd.cut",{"path":2275,"title":2276,"kind":2244},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcalculate-percent-of-total-per-group-in-pandas","Calculate Percent of Total per Group in pandas",{"path":2278,"title":2279,"kind":2244},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fcount-distinct-values-per-group-in-pandas","Count Distinct Values per Group in pandas",1790972090744]