[{"data":1,"prerenderedAt":2492},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-with-multiple-criteria-in-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-with-multiple-criteria-in-pandas":2459,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-with-multiple-criteria-in-pandas":2467,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-with-multiple-criteria-in-pandas":2472,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-with-multiple-criteria-in-pandas":2473},{"id":4,"title":5,"body":6,"dateModified":2425,"datePublished":2425,"description":2426,"extension":2427,"faq":2428,"keyword":2437,"kind":2438,"meta":2439,"navigation":113,"path":2448,"seo":2449,"slug":2452,"stem":2453,"topics":2454,"type":2457,"__hash__":2458},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-with-multiple-criteria-in-pandas\u002Findex.md","XLOOKUP with Multiple Criteria in pandas",{"type":7,"value":8,"toc":2412},"minimark",[9,41,46,74,81,85,628,657,661,667,811,822,932,936,951,1075,1091,1371,1378,1388,1538,1542,1545,2250,2273,2277,2321,2325,2343,2347,2357,2363,2369,2375,2379,2408],[10,11,12,13,17,18,21,22,25,26,30,31,34,35,40],"p",{},"Single-key lookups are easy in both Excel and pandas. Real lookups often need more than one key: the price of a product ",[14,15,16],"em",{},"in a region",", the rate for a grade ",[14,19,20],{},"in a year",", the discount for a customer ",[14,23,24],{},"at an order size",". In Excel the usual answer is an array trick — ",[27,28,29],"code",{},"XLOOKUP(1, (Prices[Product]=A2)*(Prices[Region]=B2), Prices[Price])"," — or a helper column that glues the keys together. Both work, both are hard to read and both silently pick the ",[14,32,33],{},"first"," match if the price list has accidental duplicates. pandas expresses the same lookups more directly, and makes the duplicate problem visible. This page translates the common multi-criteria XLOOKUP patterns: an exact match on two keys with a not-found value, a duplicate-key check, a tiered \"exact or next smaller\" match by quantity within each product and region, and a last-match lookup — and checks the results against the same XLOOKUP formulas recalculated in a workbook. It belongs to ",[36,37,39],"a",{"href":38},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002F","Excel Formula Equivalents in pandas",".",[42,43,45],"h2",{"id":44},"prerequisites","Prerequisites",[47,48,53],"pre",{"className":49,"code":50,"language":51,"meta":52,"style":52},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[27,54,55],{"__ignoreMap":52},[56,57,60,64,68,71],"span",{"class":58,"line":59},"line",1,[56,61,63],{"class":62},"sMTad","pip",[56,65,67],{"class":66},"srMev"," install",[56,69,70],{"class":66}," pandas",[56,72,73],{"class":66}," openpyxl\n",[10,75,76,77,80],{},"LibreOffice (",[27,78,79],{},"soffice",", version 24.8 or later, which supports XLOOKUP) recalculates the comparison workbook in Step 4. The data is a price list by product, region and minimum order quantity, and a set of orders to price.",[42,82,84],{"id":83},"step-1-exact-match-on-two-keys-with-a-default","Step 1 — Exact match on two keys, with a default",[47,86,90],{"className":87,"code":88,"language":89,"meta":52,"style":52},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nprices = pd.DataFrame({\n    \"Product\": [\"Bolts\", \"Bolts\", \"Bolts\", \"Bolts\", \"Nuts\", \"Nuts\", \"Washers\"],\n    \"Region\": [\"North\", \"North\", \"South\", \"South\", \"North\", \"South\", \"North\"],\n    \"MinQty\": [1, 500, 1, 500, 1, 1, 1],\n    \"Price\": [0.45, 0.39, 0.48, 0.41, 0.12, 0.13, 0.05],\n})\norders = pd.DataFrame({\n    \"Order\": [\"SO-1\", \"SO-2\", \"SO-3\", \"SO-4\", \"SO-5\", \"SO-6\"],\n    \"Product\": [\"Bolts\", \"Bolts\", \"Nuts\", \"Washers\", \"Rivets\", \"Bolts\"],\n    \"Region\": [\"North\", \"South\", \"South\", \"South\", \"North\", \"North\"],\n    \"Qty\": [120, 800, 300, 50, 10, 500],\n})\n\nbase = prices[prices[\"MinQty\"] == 1][[\"Product\", \"Region\", \"Price\"]]\npriced = orders.merge(base, on=[\"Product\", \"Region\"], how=\"left\", validate=\"many_to_one\", indicator=\"Found\")\npriced[\"Found\"] = priced[\"Found\"].eq(\"both\")\npriced[\"Price\"] = priced[\"Price\"].fillna(0)                      # XLOOKUP's if_not_found argument\nprint(priced.to_string(index=False))\n","python",[27,91,92,108,115,127,169,207,246,289,295,305,343,375,406,443,448,453,495,555,580,608],{"__ignoreMap":52},[56,93,94,98,102,105],{"class":58,"line":59},[56,95,97],{"class":96},"s-kum","import",[56,99,101],{"class":100},"skGVy"," pandas ",[56,103,104],{"class":96},"as",[56,106,107],{"class":100}," pd\n",[56,109,111],{"class":58,"line":110},2,[56,112,114],{"emptyLinePlaceholder":113},true,"\n",[56,116,118,121,124],{"class":58,"line":117},3,[56,119,120],{"class":100},"prices ",[56,122,123],{"class":96},"=",[56,125,126],{"class":100}," pd.DataFrame({\n",[56,128,130,133,136,139,142,144,146,148,150,152,154,157,159,161,163,166],{"class":58,"line":129},4,[56,131,132],{"class":66},"    \"Product\"",[56,134,135],{"class":100},": [",[56,137,138],{"class":66},"\"Bolts\"",[56,140,141],{"class":100},", ",[56,143,138],{"class":66},[56,145,141],{"class":100},[56,147,138],{"class":66},[56,149,141],{"class":100},[56,151,138],{"class":66},[56,153,141],{"class":100},[56,155,156],{"class":66},"\"Nuts\"",[56,158,141],{"class":100},[56,160,156],{"class":66},[56,162,141],{"class":100},[56,164,165],{"class":66},"\"Washers\"",[56,167,168],{"class":100},"],\n",[56,170,172,175,177,180,182,184,186,189,191,193,195,197,199,201,203,205],{"class":58,"line":171},5,[56,173,174],{"class":66},"    \"Region\"",[56,176,135],{"class":100},[56,178,179],{"class":66},"\"North\"",[56,181,141],{"class":100},[56,183,179],{"class":66},[56,185,141],{"class":100},[56,187,188],{"class":66},"\"South\"",[56,190,141],{"class":100},[56,192,188],{"class":66},[56,194,141],{"class":100},[56,196,179],{"class":66},[56,198,141],{"class":100},[56,200,188],{"class":66},[56,202,141],{"class":100},[56,204,179],{"class":66},[56,206,168],{"class":100},[56,208,210,213,215,219,221,224,226,228,230,232,234,236,238,240,242,244],{"class":58,"line":209},6,[56,211,212],{"class":66},"    \"MinQty\"",[56,214,135],{"class":100},[56,216,218],{"class":217},"sP0c6","1",[56,220,141],{"class":100},[56,222,223],{"class":217},"500",[56,225,141],{"class":100},[56,227,218],{"class":217},[56,229,141],{"class":100},[56,231,223],{"class":217},[56,233,141],{"class":100},[56,235,218],{"class":217},[56,237,141],{"class":100},[56,239,218],{"class":217},[56,241,141],{"class":100},[56,243,218],{"class":217},[56,245,168],{"class":100},[56,247,249,252,254,257,259,262,264,267,269,272,274,277,279,282,284,287],{"class":58,"line":248},7,[56,250,251],{"class":66},"    \"Price\"",[56,253,135],{"class":100},[56,255,256],{"class":217},"0.45",[56,258,141],{"class":100},[56,260,261],{"class":217},"0.39",[56,263,141],{"class":100},[56,265,266],{"class":217},"0.48",[56,268,141],{"class":100},[56,270,271],{"class":217},"0.41",[56,273,141],{"class":100},[56,275,276],{"class":217},"0.12",[56,278,141],{"class":100},[56,280,281],{"class":217},"0.13",[56,283,141],{"class":100},[56,285,286],{"class":217},"0.05",[56,288,168],{"class":100},[56,290,292],{"class":58,"line":291},8,[56,293,294],{"class":100},"})\n",[56,296,298,301,303],{"class":58,"line":297},9,[56,299,300],{"class":100},"orders ",[56,302,123],{"class":96},[56,304,126],{"class":100},[56,306,308,311,313,316,318,321,323,326,328,331,333,336,338,341],{"class":58,"line":307},10,[56,309,310],{"class":66},"    \"Order\"",[56,312,135],{"class":100},[56,314,315],{"class":66},"\"SO-1\"",[56,317,141],{"class":100},[56,319,320],{"class":66},"\"SO-2\"",[56,322,141],{"class":100},[56,324,325],{"class":66},"\"SO-3\"",[56,327,141],{"class":100},[56,329,330],{"class":66},"\"SO-4\"",[56,332,141],{"class":100},[56,334,335],{"class":66},"\"SO-5\"",[56,337,141],{"class":100},[56,339,340],{"class":66},"\"SO-6\"",[56,342,168],{"class":100},[56,344,346,348,350,352,354,356,358,360,362,364,366,369,371,373],{"class":58,"line":345},11,[56,347,132],{"class":66},[56,349,135],{"class":100},[56,351,138],{"class":66},[56,353,141],{"class":100},[56,355,138],{"class":66},[56,357,141],{"class":100},[56,359,156],{"class":66},[56,361,141],{"class":100},[56,363,165],{"class":66},[56,365,141],{"class":100},[56,367,368],{"class":66},"\"Rivets\"",[56,370,141],{"class":100},[56,372,138],{"class":66},[56,374,168],{"class":100},[56,376,378,380,382,384,386,388,390,392,394,396,398,400,402,404],{"class":58,"line":377},12,[56,379,174],{"class":66},[56,381,135],{"class":100},[56,383,179],{"class":66},[56,385,141],{"class":100},[56,387,188],{"class":66},[56,389,141],{"class":100},[56,391,188],{"class":66},[56,393,141],{"class":100},[56,395,188],{"class":66},[56,397,141],{"class":100},[56,399,179],{"class":66},[56,401,141],{"class":100},[56,403,179],{"class":66},[56,405,168],{"class":100},[56,407,409,412,414,417,419,422,424,427,429,432,434,437,439,441],{"class":58,"line":408},13,[56,410,411],{"class":66},"    \"Qty\"",[56,413,135],{"class":100},[56,415,416],{"class":217},"120",[56,418,141],{"class":100},[56,420,421],{"class":217},"800",[56,423,141],{"class":100},[56,425,426],{"class":217},"300",[56,428,141],{"class":100},[56,430,431],{"class":217},"50",[56,433,141],{"class":100},[56,435,436],{"class":217},"10",[56,438,141],{"class":100},[56,440,223],{"class":217},[56,442,168],{"class":100},[56,444,446],{"class":58,"line":445},14,[56,447,294],{"class":100},[56,449,451],{"class":58,"line":450},15,[56,452,114],{"emptyLinePlaceholder":113},[56,454,456,459,461,464,467,470,473,476,479,482,484,487,489,492],{"class":58,"line":455},16,[56,457,458],{"class":100},"base ",[56,460,123],{"class":96},[56,462,463],{"class":100}," prices[prices[",[56,465,466],{"class":66},"\"MinQty\"",[56,468,469],{"class":100},"] ",[56,471,472],{"class":96},"==",[56,474,475],{"class":217}," 1",[56,477,478],{"class":100},"][[",[56,480,481],{"class":66},"\"Product\"",[56,483,141],{"class":100},[56,485,486],{"class":66},"\"Region\"",[56,488,141],{"class":100},[56,490,491],{"class":66},"\"Price\"",[56,493,494],{"class":100},"]]\n",[56,496,498,501,503,506,510,512,515,517,519,521,524,527,529,532,534,537,539,542,544,547,549,552],{"class":58,"line":497},17,[56,499,500],{"class":100},"priced ",[56,502,123],{"class":96},[56,504,505],{"class":100}," orders.merge(base, ",[56,507,509],{"class":508},"sa561","on",[56,511,123],{"class":96},[56,513,514],{"class":100},"[",[56,516,481],{"class":66},[56,518,141],{"class":100},[56,520,486],{"class":66},[56,522,523],{"class":100},"], ",[56,525,526],{"class":508},"how",[56,528,123],{"class":96},[56,530,531],{"class":66},"\"left\"",[56,533,141],{"class":100},[56,535,536],{"class":508},"validate",[56,538,123],{"class":96},[56,540,541],{"class":66},"\"many_to_one\"",[56,543,141],{"class":100},[56,545,546],{"class":508},"indicator",[56,548,123],{"class":96},[56,550,551],{"class":66},"\"Found\"",[56,553,554],{"class":100},")\n",[56,556,558,561,563,565,567,570,572,575,578],{"class":58,"line":557},18,[56,559,560],{"class":100},"priced[",[56,562,551],{"class":66},[56,564,469],{"class":100},[56,566,123],{"class":96},[56,568,569],{"class":100}," priced[",[56,571,551],{"class":66},[56,573,574],{"class":100},"].eq(",[56,576,577],{"class":66},"\"both\"",[56,579,554],{"class":100},[56,581,583,585,587,589,591,593,595,598,601,604],{"class":58,"line":582},19,[56,584,560],{"class":100},[56,586,491],{"class":66},[56,588,469],{"class":100},[56,590,123],{"class":96},[56,592,569],{"class":100},[56,594,491],{"class":66},[56,596,597],{"class":100},"].fillna(",[56,599,600],{"class":217},"0",[56,602,603],{"class":100},")                      ",[56,605,607],{"class":606},"s-wDw","# XLOOKUP's if_not_found argument\n",[56,609,611,614,617,620,622,625],{"class":58,"line":610},20,[56,612,613],{"class":217},"print",[56,615,616],{"class":100},"(priced.to_string(",[56,618,619],{"class":508},"index",[56,621,123],{"class":96},[56,623,624],{"class":217},"False",[56,626,627],{"class":100},"))\n",[10,629,630,633,634,637,638,641,642,645,646,649,650,653,654,656],{},[27,631,632],{},"merge(on=[\"Product\", \"Region\"])"," is the multi-criteria lookup: a row matches only when both columns match, which is exactly what ",[27,635,636],{},"(Product=x)*(Region=y)"," means in the Excel array formula. ",[27,639,640],{},"how=\"left\""," keeps every order, matched or not, and ",[27,643,644],{},"fillna"," plays the role of XLOOKUP's ",[27,647,648],{},"if_not_found"," argument. The ",[14,651,652],{},"Found"," flag, from the merge's ",[27,655,546],{}," option, records which rows matched, because a default of 0 is indistinguishable from a real price of 0 otherwise. Washers in the South and Rivets anywhere are not in the price list and come back unpriced. Key columns must match exactly, including case and stray spaces: \"North \" with a trailing space is a different region to pandas, just as it is to Excel, so clean both sides' keys first — trimming and consistent capitalisation — when the data comes from people typing.",[42,658,660],{"id":659},"step-2-make-duplicate-keys-an-error","Step 2 — Make duplicate keys an error",[10,662,663,666],{},[27,664,665],{},"validate=\"many_to_one\""," checks that each Product–Region pair appears at most once in the lookup table, and raises if it does not:",[47,668,670],{"className":87,"code":669,"language":89,"meta":52,"style":52},"dirty = pd.concat([base, pd.DataFrame({\"Product\": [\"Nuts\"], \"Region\": [\"South\"], \"Price\": [0.15]})])\ntry:\n    orders.merge(dirty, on=[\"Product\", \"Region\"], how=\"left\", validate=\"many_to_one\")\nexcept pd.errors.MergeError as e:\n    print(\"MergeError:\", e)\nprint(dirty[dirty.duplicated([\"Product\", \"Region\"], keep=False)].to_string(index=False))\n",[27,671,672,708,716,751,764,778],{"__ignoreMap":52},[56,673,674,677,679,682,684,686,688,690,692,694,696,698,700,702,705],{"class":58,"line":59},[56,675,676],{"class":100},"dirty ",[56,678,123],{"class":96},[56,680,681],{"class":100}," pd.concat([base, pd.DataFrame({",[56,683,481],{"class":66},[56,685,135],{"class":100},[56,687,156],{"class":66},[56,689,523],{"class":100},[56,691,486],{"class":66},[56,693,135],{"class":100},[56,695,188],{"class":66},[56,697,523],{"class":100},[56,699,491],{"class":66},[56,701,135],{"class":100},[56,703,704],{"class":217},"0.15",[56,706,707],{"class":100},"]})])\n",[56,709,710,713],{"class":58,"line":110},[56,711,712],{"class":96},"try",[56,714,715],{"class":100},":\n",[56,717,718,721,723,725,727,729,731,733,735,737,739,741,743,745,747,749],{"class":58,"line":117},[56,719,720],{"class":100},"    orders.merge(dirty, ",[56,722,509],{"class":508},[56,724,123],{"class":96},[56,726,514],{"class":100},[56,728,481],{"class":66},[56,730,141],{"class":100},[56,732,486],{"class":66},[56,734,523],{"class":100},[56,736,526],{"class":508},[56,738,123],{"class":96},[56,740,531],{"class":66},[56,742,141],{"class":100},[56,744,536],{"class":508},[56,746,123],{"class":96},[56,748,541],{"class":66},[56,750,554],{"class":100},[56,752,753,756,759,761],{"class":58,"line":129},[56,754,755],{"class":96},"except",[56,757,758],{"class":100}," pd.errors.MergeError ",[56,760,104],{"class":96},[56,762,763],{"class":100}," e:\n",[56,765,766,769,772,775],{"class":58,"line":171},[56,767,768],{"class":217},"    print",[56,770,771],{"class":100},"(",[56,773,774],{"class":66},"\"MergeError:\"",[56,776,777],{"class":100},", e)\n",[56,779,780,782,785,787,789,791,793,796,798,800,803,805,807,809],{"class":58,"line":209},[56,781,613],{"class":217},[56,783,784],{"class":100},"(dirty[dirty.duplicated([",[56,786,481],{"class":66},[56,788,141],{"class":100},[56,790,486],{"class":66},[56,792,523],{"class":100},[56,794,795],{"class":508},"keep",[56,797,123],{"class":96},[56,799,624],{"class":217},[56,801,802],{"class":100},")].to_string(",[56,804,619],{"class":508},[56,806,123],{"class":96},[56,808,624],{"class":217},[56,810,627],{"class":100},[10,812,813,814,817,818,40],{},"With a second price for Nuts in the South, Excel's XLOOKUP would quietly return the first one; an unvalidated merge would be worse, duplicating the order row so it is counted twice. The validation stops the lookup and the ",[27,815,816],{},"duplicated"," check shows the offending rows. A duplicate key in a price list is a data problem to fix at source, not something the lookup should choose between. More on this guard in ",[36,819,821],{"href":820},"\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",[823,824,832,833,832,837,832,841,832,848,832,858,832,869,832,876,832,882,832,887,832,891,832,895,832,899,832,904,832,911,832,916,832,920,832,923,832,926,832,929],"svg",{"viewBox":825,"role":826,"ariaLabelledBy":827,"xmlns":830,"style":831},"4 0 752 233","img",[828,829],"xl-excel-pandas-t","xl-excel-pandas-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  ",[834,835,836],"title",{"id":828},"The same lookup in Excel and pandas",[838,839,840],"desc",{"id":829},"Excel matches several criteria with an array product inside XLOOKUP or a concatenated helper key, and returns the first match if keys are duplicated. pandas merges on several columns, can return a default for missing keys, and with validate many_to_one raises an error on duplicate keys.",[842,843],"rect",{"x":844,"y":600,"width":845,"height":846,"fill":847},"4","752","233","#ffffff",[842,849],{"x":850,"y":851,"width":852,"height":853,"rx":854,"fill":855,"stroke":856,"style":857},"20.0","16.0","351.0","201.0","14","#fdefd8","var(--gold,#b4740a)","stroke-width:2px",[859,860,862],"g",{"transform":861},"translate(182.5,29.0) scale(1.083)",[863,864],"path",{"d":865,"fill":866,"stroke":867,"style":868},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16","none","var(--gold-ink,#7a4e06)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[870,871,875],"text",{"x":872,"y":873,"style":874},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","XLOOKUP(1,(A=x)*(B=y),C)",[58,877],{"x1":878,"y1":879,"x2":880,"y2":879,"stroke":856,"style":881},"36.0","91.0","355.0","stroke-width:1px",[870,883,886],{"x":872,"y":884,"style":885},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","array product of conditions",[870,888,890],{"x":872,"y":889,"style":885},"137.0","if_not_found argument",[870,892,894],{"x":872,"y":893,"style":885},"163.0","first match on duplicates",[870,896,898],{"x":872,"y":897,"style":885},"189.0","hard to read",[842,900],{"x":901,"y":851,"width":852,"height":853,"rx":854,"fill":902,"stroke":903,"style":857},"389.0","#d9f4f1","var(--teal,#0f9488)",[859,905,907],{"transform":906},"translate(551.5,29.0) scale(1.083)",[863,908],{"d":909,"fill":866,"stroke":910,"style":868},"M12 3c-4 0-4 1.6-4 3v2h4.2v1H6c-2 0-3 1.4-3 4s1 4 3 4h2v-2.6c0-1.6 1.3-2.9 2.9-2.9h4.2c1.3 0 2.4-1.1 2.4-2.4V6c0-1.6-1.8-3-5.5-3z M10 5.5h.01 M12 21c4 0 4-1.6 4-3v-2h-4.2v-1H18c2 0 3-1.4 3-4s-1-4-3-4h-2v2.6c0 1.6-1.3 2.9-2.9 2.9H8.9c-1.3 0-2.4 1.1-2.4 2.4V18c0 1.6 1.8 3 5.5 3z M14 18.5h.01","var(--teal-ink,#0b6157)",[870,912,915],{"x":913,"y":873,"style":914},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","merge(on=[A, B])",[58,917],{"x1":918,"y1":879,"x2":919,"y2":879,"stroke":903,"style":881},"405.0","724.0",[870,921,922],{"x":913,"y":884,"style":885},"all key columns at once",[870,924,925],{"x":913,"y":889,"style":885},"fillna for the default",[870,927,928],{"x":913,"y":893,"style":885},"validate='many_to_one'",[870,930,931],{"x":913,"y":897,"style":885},"Found flag for audit",[42,933,935],{"id":934},"step-3-tiered-prices-exact-or-next-smaller-within-each-key","Step 3 — Tiered prices: exact or next smaller, within each key",[10,937,938,939,942,943,946,947,950],{},"Price breaks are XLOOKUP's match mode −1, \"exact match or next smaller\": the price for 800 units is the row with the largest minimum quantity not above 800, ",[14,940,941],{},"for the same product and region",". That is ",[27,944,945],{},"merge_asof"," with ",[27,948,949],{},"by",":",[47,952,954],{"className":87,"code":953,"language":89,"meta":52,"style":52},"tiered = pd.merge_asof(orders.sort_values(\"Qty\"), prices.sort_values(\"MinQty\"),\n                       left_on=\"Qty\", right_on=\"MinQty\", by=[\"Product\", \"Region\"], direction=\"backward\")\ntiered = tiered.sort_values(\"Order\")[[\"Order\", \"Product\", \"Region\", \"Qty\", \"MinQty\", \"Price\"]]\nprint(tiered.to_string(index=False))\n",[27,955,956,977,1021,1060],{"__ignoreMap":52},[56,957,958,961,963,966,969,972,974],{"class":58,"line":59},[56,959,960],{"class":100},"tiered ",[56,962,123],{"class":96},[56,964,965],{"class":100}," pd.merge_asof(orders.sort_values(",[56,967,968],{"class":66},"\"Qty\"",[56,970,971],{"class":100},"), prices.sort_values(",[56,973,466],{"class":66},[56,975,976],{"class":100},"),\n",[56,978,979,982,984,986,988,991,993,995,997,999,1001,1003,1005,1007,1009,1011,1014,1016,1019],{"class":58,"line":110},[56,980,981],{"class":508},"                       left_on",[56,983,123],{"class":96},[56,985,968],{"class":66},[56,987,141],{"class":100},[56,989,990],{"class":508},"right_on",[56,992,123],{"class":96},[56,994,466],{"class":66},[56,996,141],{"class":100},[56,998,949],{"class":508},[56,1000,123],{"class":96},[56,1002,514],{"class":100},[56,1004,481],{"class":66},[56,1006,141],{"class":100},[56,1008,486],{"class":66},[56,1010,523],{"class":100},[56,1012,1013],{"class":508},"direction",[56,1015,123],{"class":96},[56,1017,1018],{"class":66},"\"backward\"",[56,1020,554],{"class":100},[56,1022,1023,1025,1027,1030,1033,1036,1038,1040,1042,1044,1046,1048,1050,1052,1054,1056,1058],{"class":58,"line":117},[56,1024,960],{"class":100},[56,1026,123],{"class":96},[56,1028,1029],{"class":100}," tiered.sort_values(",[56,1031,1032],{"class":66},"\"Order\"",[56,1034,1035],{"class":100},")[[",[56,1037,1032],{"class":66},[56,1039,141],{"class":100},[56,1041,481],{"class":66},[56,1043,141],{"class":100},[56,1045,486],{"class":66},[56,1047,141],{"class":100},[56,1049,968],{"class":66},[56,1051,141],{"class":100},[56,1053,466],{"class":66},[56,1055,141],{"class":100},[56,1057,491],{"class":66},[56,1059,494],{"class":100},[56,1061,1062,1064,1067,1069,1071,1073],{"class":58,"line":129},[56,1063,613],{"class":217},[56,1065,1066],{"class":100},"(tiered.to_string(",[56,1068,619],{"class":508},[56,1070,123],{"class":96},[56,1072,624],{"class":217},[56,1074,627],{"class":100},[10,1076,1077,1079,1080,1083,1084,1086,1087,1090],{},[27,1078,945],{}," matches each order to the last price row whose ",[27,1081,1082],{},"MinQty"," is at or below the order's quantity, and ",[27,1085,949],{}," restricts the search to rows with the same product and region — the extra criteria. Both frames must be sorted on the matching column. SO-2's 800 South bolts get the 500+ price of 0.41, SO-6's exactly 500 North bolts get 0.39 (an exact match on the break), and SO-1's 120 North bolts the base 0.45. Unmatched combinations come back as ",[27,1088,1089],{},"NaN"," again.",[823,1092,832,1098,832,1101,832,1104,832,1109,832,1116,832,1120,832,1126,832,1130,832,1134,832,1138,832,1142,832,1146,832,1150,832,1154,832,1158,832,1162,832,1166,832,1171,832,1175,832,1177,832,1180,832,1182,832,1185,832,1187,832,1190,832,1192,832,1195,832,1198,832,1202,832,1204,832,1207,832,1209,832,1212,832,1214,832,1217,832,1219,832,1221,832,1223,832,1225,832,1228,832,1232,832,1234,832,1237,832,1239,832,1241,832,1243,832,1246,832,1248,832,1250,832,1252,832,1255,832,1258,832,1261,832,1263,832,1266,832,1268,832,1271,832,1273,832,1275,832,1277,832,1279,832,1281,832,1283,832,1286,832,1290,832,1292,832,1295,832,1297,832,1300,832,1302,832,1304,832,1306,832,1308,832,1310,832,1314,832,1317,832,1321,832,1323,832,1326,832,1328,832,1331,832,1333,832,1335,832,1337,832,1339,832,1341,832,1343,832,1346,832,1350,832,1352,832,1355,832,1357,832,1359,832,1361,832,1363,832,1365,832,1367,832,1369],{"viewBox":1093,"role":826,"ariaLabelledBy":1094,"xmlns":830,"style":1097},"193 0 374 252",[1095,1096],"xl-tiers-t","xl-tiers-d","width:100%;max-width:374px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[834,1099,1100],{"id":1095},"Tiered price lookup by product, region and quantity",[838,1102,1103],{"id":1096},"Each order is matched to the price row with the same product and region and the largest minimum quantity not above the order quantity. 120 North bolts get the base price 0.45, 500 North bolts the break price 0.39, 800 South bolts the South break price 0.41. Rivets and South washers have no price.",[842,1105],{"x":1106,"y":600,"width":1107,"height":1108,"fill":847},"193","374","252",[842,1110],{"x":1111,"y":851,"width":1112,"height":1113,"rx":600,"fill":1114,"stroke":1115,"style":881},"209.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)",[842,1117],{"x":1118,"y":851,"width":1119,"height":1113,"rx":600,"fill":1114,"stroke":1115,"style":881},"241.0","59.4",[870,1121,1125],{"x":1122,"y":1123,"style":1124},"270.7","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[842,1127],{"x":1128,"y":851,"width":1129,"height":1113,"rx":600,"fill":1114,"stroke":1115,"style":881},"300.4","79.0",[870,1131,1133],{"x":1132,"y":1123,"style":1124},"339.9","B",[842,1135],{"x":1136,"y":851,"width":1137,"height":1113,"rx":600,"fill":1114,"stroke":1115,"style":881},"379.4","66.7",[870,1139,1141],{"x":1140,"y":1123,"style":1124},"412.8","C",[842,1143],{"x":1144,"y":851,"width":1145,"height":1113,"rx":600,"fill":1114,"stroke":1115,"style":881},"446.2","49.2",[870,1147,1149],{"x":1148,"y":1123,"style":1124},"470.8","D",[842,1151],{"x":1152,"y":851,"width":1153,"height":1113,"rx":600,"fill":1114,"stroke":1115,"style":881},"495.4","55.6",[870,1155,1157],{"x":1156,"y":1123,"style":1124},"523.2","E",[842,1159],{"x":1111,"y":1160,"width":1112,"height":1161,"rx":600,"fill":1114,"stroke":1115,"style":881},"40.0","28.0",[870,1163,218],{"x":1164,"y":1165,"style":1124},"225.0","59.0",[842,1167],{"x":1118,"y":1160,"width":1119,"height":1161,"rx":600,"fill":1168,"stroke":1169,"style":1170},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[870,1172,1174],{"x":1122,"y":1165,"style":1173},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Order",[842,1176],{"x":1128,"y":1160,"width":1129,"height":1161,"rx":600,"fill":1168,"stroke":1169,"style":1170},[870,1178,1179],{"x":1132,"y":1165,"style":1173},"Product",[842,1181],{"x":1136,"y":1160,"width":1137,"height":1161,"rx":600,"fill":1168,"stroke":1169,"style":1170},[870,1183,1184],{"x":1140,"y":1165,"style":1173},"Region",[842,1186],{"x":1144,"y":1160,"width":1145,"height":1161,"rx":600,"fill":1168,"stroke":1169,"style":1170},[870,1188,1189],{"x":1148,"y":1165,"style":1173},"Qty",[842,1191],{"x":1152,"y":1160,"width":1153,"height":1161,"rx":600,"fill":1168,"stroke":1169,"style":1170},[870,1193,1194],{"x":1156,"y":1165,"style":1173},"Price",[842,1196],{"x":1111,"y":1197,"width":1112,"height":1161,"rx":600,"fill":1114,"stroke":1115,"style":881},"68.0",[870,1199,1201],{"x":1164,"y":1200,"style":1124},"87.0","2",[842,1203],{"x":1118,"y":1197,"width":1119,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1205,1206],{"x":1122,"y":1200,"style":885},"SO-1",[842,1208],{"x":1128,"y":1197,"width":1129,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1210,1211],{"x":1132,"y":1200,"style":885},"Bolts",[842,1213],{"x":1136,"y":1197,"width":1137,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1215,1216],{"x":1140,"y":1200,"style":885},"North",[842,1218],{"x":1144,"y":1197,"width":1145,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1220,416],{"x":1148,"y":1200,"style":885},[842,1222],{"x":1152,"y":1197,"width":1153,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1224,256],{"x":1156,"y":1200,"style":885},[842,1226],{"x":1111,"y":1227,"width":1112,"height":1161,"rx":600,"fill":1114,"stroke":1115,"style":881},"96.0",[870,1229,1231],{"x":1164,"y":1230,"style":1124},"115.0","3",[842,1233],{"x":1118,"y":1227,"width":1119,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1235,1236],{"x":1122,"y":1230,"style":885},"SO-2",[842,1238],{"x":1128,"y":1227,"width":1129,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1240,1211],{"x":1132,"y":1230,"style":885},[842,1242],{"x":1136,"y":1227,"width":1137,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1244,1245],{"x":1140,"y":1230,"style":885},"South",[842,1247],{"x":1144,"y":1227,"width":1145,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1249,421],{"x":1148,"y":1230,"style":885},[842,1251],{"x":1152,"y":1227,"width":1153,"height":1161,"rx":600,"fill":902,"stroke":903,"style":1170},[870,1253,271],{"x":1156,"y":1230,"style":1254},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[842,1256],{"x":1111,"y":1257,"width":1112,"height":1161,"rx":600,"fill":1114,"stroke":1115,"style":881},"124.0",[870,1259,844],{"x":1164,"y":1260,"style":1124},"143.0",[842,1262],{"x":1118,"y":1257,"width":1119,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1264,1265],{"x":1122,"y":1260,"style":885},"SO-3",[842,1267],{"x":1128,"y":1257,"width":1129,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1269,1270],{"x":1132,"y":1260,"style":885},"Nuts",[842,1272],{"x":1136,"y":1257,"width":1137,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1274,1245],{"x":1140,"y":1260,"style":885},[842,1276],{"x":1144,"y":1257,"width":1145,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1278,426],{"x":1148,"y":1260,"style":885},[842,1280],{"x":1152,"y":1257,"width":1153,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1282,281],{"x":1156,"y":1260,"style":885},[842,1284],{"x":1111,"y":1285,"width":1112,"height":1161,"rx":600,"fill":1114,"stroke":1115,"style":881},"152.0",[870,1287,1289],{"x":1164,"y":1288,"style":1124},"171.0","5",[842,1291],{"x":1118,"y":1285,"width":1119,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1293,1294],{"x":1122,"y":1288,"style":885},"SO-4",[842,1296],{"x":1128,"y":1285,"width":1129,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1298,1299],{"x":1132,"y":1288,"style":885},"Washers",[842,1301],{"x":1136,"y":1285,"width":1137,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1303,1245],{"x":1140,"y":1288,"style":885},[842,1305],{"x":1144,"y":1285,"width":1145,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1307,431],{"x":1148,"y":1288,"style":885},[842,1309],{"x":1152,"y":1285,"width":1153,"height":1161,"rx":600,"fill":855,"stroke":856,"style":1170},[870,1311,1313],{"x":1156,"y":1288,"style":1312},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","—",[842,1315],{"x":1111,"y":1316,"width":1112,"height":1161,"rx":600,"fill":1114,"stroke":1115,"style":881},"180.0",[870,1318,1320],{"x":1164,"y":1319,"style":1124},"199.0","6",[842,1322],{"x":1118,"y":1316,"width":1119,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1324,1325],{"x":1122,"y":1319,"style":885},"SO-5",[842,1327],{"x":1128,"y":1316,"width":1129,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1329,1330],{"x":1132,"y":1319,"style":885},"Rivets",[842,1332],{"x":1136,"y":1316,"width":1137,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1334,1216],{"x":1140,"y":1319,"style":885},[842,1336],{"x":1144,"y":1316,"width":1145,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1338,436],{"x":1148,"y":1319,"style":885},[842,1340],{"x":1152,"y":1316,"width":1153,"height":1161,"rx":600,"fill":855,"stroke":856,"style":1170},[870,1342,1313],{"x":1156,"y":1319,"style":1312},[842,1344],{"x":1111,"y":1345,"width":1112,"height":1161,"rx":600,"fill":1114,"stroke":1115,"style":881},"208.0",[870,1347,1349],{"x":1164,"y":1348,"style":1124},"227.0","7",[842,1351],{"x":1118,"y":1345,"width":1119,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1353,1354],{"x":1122,"y":1348,"style":885},"SO-6",[842,1356],{"x":1128,"y":1345,"width":1129,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1358,1211],{"x":1132,"y":1348,"style":885},[842,1360],{"x":1136,"y":1345,"width":1137,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1362,1216],{"x":1140,"y":1348,"style":885},[842,1364],{"x":1144,"y":1345,"width":1145,"height":1161,"rx":600,"fill":847,"stroke":1115,"style":881},[870,1366,223],{"x":1148,"y":1348,"style":885},[842,1368],{"x":1152,"y":1345,"width":1153,"height":1161,"rx":600,"fill":902,"stroke":903,"style":1170},[870,1370,261],{"x":1156,"y":1348,"style":1254},[42,1372,1374,1375],{"id":1373},"returning-several-columns-and-why-not-apply","Returning several columns, and why not ",[27,1376,1377],{},"apply",[10,1379,1380,1381,1384,1385,1387],{},"An Excel XLOOKUP can return a whole row of results — price, currency and unit — by giving a multi-column return array. A merge does that naturally: every column of the lookup table that is not a key comes along, so selecting ",[27,1382,1383],{},"[\"Product\", \"Region\", \"Price\", \"Currency\", \"Unit\"]"," before merging returns all three at once. Resist the temptation to write the lookup as a function applied row by row, filtering the price table for each order; it reads like the Excel formula but runs one filter per row, which on tens of thousands of orders is slower by orders of magnitude than a single merge, and it hides the duplicate-key problem because each filter just takes the first row. The merge also makes the result easy to audit: with ",[27,1386,546],{},", every row says whether it was found, which the row-by-row version would need extra code to report.",[823,1389,832,1394,832,1397,832,1400,832,1403,832,1408,832,1416,832,1422,832,1426,832,1431,832,1436,832,1441,832,1444,832,1451,832,1456,832,1459,832,1462,832,1466,832,1469,832,1472,832,1478,832,1484,832,1488,832,1492,832,1495,832,1498,832,1504,832,1509,832,1512,832,1515,832,1519,832,1522,832,1525,832,1531,832,1535],{"viewBox":1390,"role":826,"ariaLabelledBy":1391,"xmlns":830,"style":831},"4 0 752 142",[1392,1393],"xl-flow-t","xl-flow-d",[834,1395,1396],{"id":1392},"A multi-criteria lookup in pandas",[838,1398,1399],{"id":1393},"Select the key and return columns from the lookup table, merge on all key columns with a left join, validate that keys are unique, flag which rows were found, fill the not-found default, and for tiered lookups use merge_asof with by on the exact keys.",[842,1401],{"x":844,"y":600,"width":845,"height":1402,"fill":847},"142",[842,1404],{"x":850,"y":851,"width":1405,"height":1406,"rx":1407,"fill":1114,"stroke":1115,"style":857},"115.2","110.5","12",[859,1409,1411],{"transform":1410},"translate(65.6,29.0) scale(1.000)",[863,1412],{"d":1413,"fill":866,"stroke":1414,"style":1415},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[870,1417,1421],{"x":1418,"y":1419,"style":1420},"77.6","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Select",[870,1423,1425],{"x":1418,"y":1424,"style":1420},"93.0","columns",[870,1427,1430],{"x":1418,"y":1428,"style":1429},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","keys + returns",[58,1432],{"x1":1433,"y1":1434,"x2":1435,"y2":1434,"stroke":1169,"style":857},"140.2","71.2","162.2",[1437,1438],"polygon",{"points":1439,"fill":1440},"166.2,71.2 157.2,76.2 157.2,66.2","#5b5cf0",[842,1442],{"x":1443,"y":851,"width":1405,"height":1406,"rx":1407,"fill":1168,"stroke":1169,"style":857},"171.2",[859,1445,1447],{"transform":1446},"translate(216.8,29.0) scale(1.000)",[863,1448],{"d":1449,"fill":866,"stroke":1450,"style":1415},"M6 4v5a5 5 0 0 0 5 5h7 M6 20v-5 M15 11l3 3-3 3","var(--brand-strong,#4338ca)",[870,1452,1455],{"x":1453,"y":1419,"style":1454},"228.8","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","merge on",[870,1457,1458],{"x":1453,"y":1424,"style":1454},"keys",[870,1460,1461],{"x":1453,"y":1428,"style":1429},"how='left'",[58,1463],{"x1":1464,"y1":1434,"x2":1465,"y2":1434,"stroke":1169,"style":857},"291.4","313.4",[1437,1467],{"points":1468,"fill":1440},"317.4,71.2 308.4,76.2 308.4,66.2",[842,1470],{"x":1471,"y":851,"width":1405,"height":1406,"rx":1407,"fill":855,"stroke":856,"style":857},"322.4",[859,1473,1475],{"transform":1474},"translate(368.0,37.8) scale(1.000)",[863,1476],{"d":1477,"fill":866,"stroke":867,"style":1415},"M5 12.5l4.5 4.5L19 7",[870,1479,1483],{"x":1480,"y":1481,"style":1482},"380.0","84.2","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Validate",[870,1485,1487],{"x":1480,"y":1486,"style":1429},"103.8","many_to_one",[58,1489],{"x1":1490,"y1":1434,"x2":1491,"y2":1434,"stroke":1169,"style":857},"442.6","464.6",[1437,1493],{"points":1494,"fill":1440},"468.6,71.2 459.6,76.2 459.6,66.2",[842,1496],{"x":1497,"y":851,"width":1405,"height":1406,"rx":1407,"fill":902,"stroke":903,"style":857},"473.6",[859,1499,1501],{"transform":1500},"translate(519.2,29.0) scale(1.000)",[863,1502],{"d":1503,"fill":866,"stroke":910,"style":1415},"M3 5h18l-7 8v6l-4 2v-8z",[870,1505,1508],{"x":1506,"y":1419,"style":1507},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Flag and",[870,1510,1511],{"x":1506,"y":1424,"style":1507},"default",[870,1513,1514],{"x":1506,"y":1428,"style":1429},"indicator, fillna",[58,1516],{"x1":1517,"y1":1434,"x2":1518,"y2":1434,"stroke":1169,"style":857},"593.8","615.8",[1437,1520],{"points":1521,"fill":1440},"619.8,71.2 610.8,76.2 610.8,66.2",[842,1523],{"x":1524,"y":851,"width":1405,"height":1406,"rx":1407,"fill":1168,"stroke":1169,"style":857},"624.8",[859,1526,1528],{"transform":1527},"translate(670.4,37.8) scale(1.000)",[863,1529],{"d":1530,"fill":866,"stroke":1450,"style":1415},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5",[870,1532,1534],{"x":1533,"y":1481,"style":1454},"682.4","Tiers",[870,1536,1537],{"x":1533,"y":1486,"style":1429},"merge_asof(by=…)",[42,1539,1541],{"id":1540},"step-4-check-against-xlookup-in-a-workbook","Step 4 — Check against XLOOKUP in a workbook",[10,1543,1544],{},"Write the orders and prices to a workbook with the equivalent XLOOKUP formulas, recalculate, and compare:",[47,1546,1548],{"className":87,"code":1547,"language":89,"meta":52,"style":52},"import subprocess\nfrom openpyxl import Workbook, load_workbook\n\nwb = Workbook()\nws = wb.active\nws.title = \"Orders\"\npl = wb.create_sheet(\"Prices\")\npl.append(list(prices.columns))\nfor row in prices.itertuples(index=False):\n    pl.append(list(row))\nws.append(list(orders.columns) + [\"Base price\", \"Tier price\"])\nn = len(prices) + 1\nfor r, row in enumerate(orders.itertuples(index=False), start=2):\n    ws.append(list(row))\n    ws[f\"E{r}\"] = (f\"=_xlfn.XLOOKUP(1,(Prices!$A$2:$A${n}=B{r})*(Prices!$B$2:$B${n}=C{r})*(Prices!$C$2:$C${n}=1),\"\n                   f\"Prices!$D$2:$D${n},0)\")\n    ws[f\"F{r}\"] = (f\"=_xlfn.XLOOKUP(1,(Prices!$A$2:$A${n}=B{r})*(Prices!$B$2:$B${n}=C{r})*(Prices!$C$2:$C${n}\u003C=D{r}),\"\n                   f\"Prices!$D$2:$D${n},0,0,-1)\")\nwb.save(\"lookup_check.xlsx\")\nsubprocess.run([\"soffice\", \"--headless\", \"--convert-to\", \"xlsx\", \"--outdir\", \"calc\", \"lookup_check.xlsx\"], check=True, capture_output=True)\ncalc = load_workbook(\"calc\u002Flookup_check.xlsx\", data_only=True)[\"Orders\"]\nexcel = pd.DataFrame([[c.value for c in row] for row in calc.iter_rows(min_row=2, max_col=6)], columns=list(orders.columns) + [\"Base\", \"Tier\"])\ncompare = excel.assign(pandas_base=priced[\"Price\"].values, pandas_tier=tiered.set_index(\"Order\").loc[excel[\"Order\"], \"Price\"].fillna(0).values)\nprint(compare[[\"Order\", \"Base\", \"pandas_base\", \"Tier\", \"pandas_tier\"]].to_string(index=False))\nprint(\"all equal:\", (compare[\"Base\"] == compare[\"pandas_base\"]).all() and (compare[\"Tier\"] == compare[\"pandas_tier\"]).all())\n",[27,1549,1550,1557,1570,1574,1584,1594,1604,1619,1630,1653,1663,1690,1708,1741,1750,1832,1851,1928,1945,1955,2013,2045,2116,2165,2204],{"__ignoreMap":52},[56,1551,1552,1554],{"class":58,"line":59},[56,1553,97],{"class":96},[56,1555,1556],{"class":100}," subprocess\n",[56,1558,1559,1562,1565,1567],{"class":58,"line":110},[56,1560,1561],{"class":96},"from",[56,1563,1564],{"class":100}," openpyxl ",[56,1566,97],{"class":96},[56,1568,1569],{"class":100}," Workbook, load_workbook\n",[56,1571,1572],{"class":58,"line":117},[56,1573,114],{"emptyLinePlaceholder":113},[56,1575,1576,1579,1581],{"class":58,"line":129},[56,1577,1578],{"class":100},"wb ",[56,1580,123],{"class":96},[56,1582,1583],{"class":100}," Workbook()\n",[56,1585,1586,1589,1591],{"class":58,"line":171},[56,1587,1588],{"class":100},"ws ",[56,1590,123],{"class":96},[56,1592,1593],{"class":100}," wb.active\n",[56,1595,1596,1599,1601],{"class":58,"line":209},[56,1597,1598],{"class":100},"ws.title ",[56,1600,123],{"class":96},[56,1602,1603],{"class":66}," \"Orders\"\n",[56,1605,1606,1609,1611,1614,1617],{"class":58,"line":248},[56,1607,1608],{"class":100},"pl ",[56,1610,123],{"class":96},[56,1612,1613],{"class":100}," wb.create_sheet(",[56,1615,1616],{"class":66},"\"Prices\"",[56,1618,554],{"class":100},[56,1620,1621,1624,1627],{"class":58,"line":291},[56,1622,1623],{"class":100},"pl.append(",[56,1625,1626],{"class":217},"list",[56,1628,1629],{"class":100},"(prices.columns))\n",[56,1631,1632,1635,1638,1641,1644,1646,1648,1650],{"class":58,"line":297},[56,1633,1634],{"class":96},"for",[56,1636,1637],{"class":100}," row ",[56,1639,1640],{"class":96},"in",[56,1642,1643],{"class":100}," prices.itertuples(",[56,1645,619],{"class":508},[56,1647,123],{"class":96},[56,1649,624],{"class":217},[56,1651,1652],{"class":100},"):\n",[56,1654,1655,1658,1660],{"class":58,"line":307},[56,1656,1657],{"class":100},"    pl.append(",[56,1659,1626],{"class":217},[56,1661,1662],{"class":100},"(row))\n",[56,1664,1665,1668,1670,1673,1676,1679,1682,1684,1687],{"class":58,"line":345},[56,1666,1667],{"class":100},"ws.append(",[56,1669,1626],{"class":217},[56,1671,1672],{"class":100},"(orders.columns) ",[56,1674,1675],{"class":96},"+",[56,1677,1678],{"class":100}," [",[56,1680,1681],{"class":66},"\"Base price\"",[56,1683,141],{"class":100},[56,1685,1686],{"class":66},"\"Tier price\"",[56,1688,1689],{"class":100},"])\n",[56,1691,1692,1695,1697,1700,1703,1705],{"class":58,"line":377},[56,1693,1694],{"class":100},"n ",[56,1696,123],{"class":96},[56,1698,1699],{"class":217}," len",[56,1701,1702],{"class":100},"(prices) ",[56,1704,1675],{"class":96},[56,1706,1707],{"class":217}," 1\n",[56,1709,1710,1712,1715,1717,1720,1723,1725,1727,1729,1732,1735,1737,1739],{"class":58,"line":408},[56,1711,1634],{"class":96},[56,1713,1714],{"class":100}," r, row ",[56,1716,1640],{"class":96},[56,1718,1719],{"class":217}," enumerate",[56,1721,1722],{"class":100},"(orders.itertuples(",[56,1724,619],{"class":508},[56,1726,123],{"class":96},[56,1728,624],{"class":217},[56,1730,1731],{"class":100},"), ",[56,1733,1734],{"class":508},"start",[56,1736,123],{"class":96},[56,1738,1201],{"class":217},[56,1740,1652],{"class":100},[56,1742,1743,1746,1748],{"class":58,"line":445},[56,1744,1745],{"class":100},"    ws.append(",[56,1747,1626],{"class":217},[56,1749,1662],{"class":100},[56,1751,1752,1755,1758,1761,1765,1768,1771,1774,1776,1778,1781,1783,1786,1788,1791,1793,1796,1798,1800,1802,1805,1807,1809,1811,1814,1816,1818,1820,1823,1825,1827,1829],{"class":58,"line":450},[56,1753,1754],{"class":100},"    ws[",[56,1756,1757],{"class":96},"f",[56,1759,1760],{"class":66},"\"E",[56,1762,1764],{"class":1763},"sSjpA","{",[56,1766,1767],{"class":100},"r",[56,1769,1770],{"class":1763},"}",[56,1772,1773],{"class":66},"\"",[56,1775,469],{"class":100},[56,1777,123],{"class":96},[56,1779,1780],{"class":100}," (",[56,1782,1757],{"class":96},[56,1784,1785],{"class":66},"\"=_xlfn.XLOOKUP(1,(Prices!$A$2:$A$",[56,1787,1764],{"class":1763},[56,1789,1790],{"class":100},"n",[56,1792,1770],{"class":1763},[56,1794,1795],{"class":66},"=B",[56,1797,1764],{"class":1763},[56,1799,1767],{"class":100},[56,1801,1770],{"class":1763},[56,1803,1804],{"class":66},")*(Prices!$B$2:$B$",[56,1806,1764],{"class":1763},[56,1808,1790],{"class":100},[56,1810,1770],{"class":1763},[56,1812,1813],{"class":66},"=C",[56,1815,1764],{"class":1763},[56,1817,1767],{"class":100},[56,1819,1770],{"class":1763},[56,1821,1822],{"class":66},")*(Prices!$C$2:$C$",[56,1824,1764],{"class":1763},[56,1826,1790],{"class":100},[56,1828,1770],{"class":1763},[56,1830,1831],{"class":66},"=1),\"\n",[56,1833,1834,1837,1840,1842,1844,1846,1849],{"class":58,"line":455},[56,1835,1836],{"class":96},"                   f",[56,1838,1839],{"class":66},"\"Prices!$D$2:$D$",[56,1841,1764],{"class":1763},[56,1843,1790],{"class":100},[56,1845,1770],{"class":1763},[56,1847,1848],{"class":66},",0)\"",[56,1850,554],{"class":100},[56,1852,1853,1855,1857,1860,1862,1864,1866,1868,1870,1872,1874,1876,1878,1880,1882,1884,1886,1888,1890,1892,1894,1896,1898,1900,1902,1904,1906,1908,1910,1912,1914,1916,1919,1921,1923,1925],{"class":58,"line":497},[56,1854,1754],{"class":100},[56,1856,1757],{"class":96},[56,1858,1859],{"class":66},"\"F",[56,1861,1764],{"class":1763},[56,1863,1767],{"class":100},[56,1865,1770],{"class":1763},[56,1867,1773],{"class":66},[56,1869,469],{"class":100},[56,1871,123],{"class":96},[56,1873,1780],{"class":100},[56,1875,1757],{"class":96},[56,1877,1785],{"class":66},[56,1879,1764],{"class":1763},[56,1881,1790],{"class":100},[56,1883,1770],{"class":1763},[56,1885,1795],{"class":66},[56,1887,1764],{"class":1763},[56,1889,1767],{"class":100},[56,1891,1770],{"class":1763},[56,1893,1804],{"class":66},[56,1895,1764],{"class":1763},[56,1897,1790],{"class":100},[56,1899,1770],{"class":1763},[56,1901,1813],{"class":66},[56,1903,1764],{"class":1763},[56,1905,1767],{"class":100},[56,1907,1770],{"class":1763},[56,1909,1822],{"class":66},[56,1911,1764],{"class":1763},[56,1913,1790],{"class":100},[56,1915,1770],{"class":1763},[56,1917,1918],{"class":66},"\u003C=D",[56,1920,1764],{"class":1763},[56,1922,1767],{"class":100},[56,1924,1770],{"class":1763},[56,1926,1927],{"class":66},"),\"\n",[56,1929,1930,1932,1934,1936,1938,1940,1943],{"class":58,"line":557},[56,1931,1836],{"class":96},[56,1933,1839],{"class":66},[56,1935,1764],{"class":1763},[56,1937,1790],{"class":100},[56,1939,1770],{"class":1763},[56,1941,1942],{"class":66},",0,0,-1)\"",[56,1944,554],{"class":100},[56,1946,1947,1950,1953],{"class":58,"line":582},[56,1948,1949],{"class":100},"wb.save(",[56,1951,1952],{"class":66},"\"lookup_check.xlsx\"",[56,1954,554],{"class":100},[56,1956,1957,1960,1963,1965,1968,1970,1973,1975,1978,1980,1983,1985,1988,1990,1992,1994,1997,1999,2002,2004,2007,2009,2011],{"class":58,"line":610},[56,1958,1959],{"class":100},"subprocess.run([",[56,1961,1962],{"class":66},"\"soffice\"",[56,1964,141],{"class":100},[56,1966,1967],{"class":66},"\"--headless\"",[56,1969,141],{"class":100},[56,1971,1972],{"class":66},"\"--convert-to\"",[56,1974,141],{"class":100},[56,1976,1977],{"class":66},"\"xlsx\"",[56,1979,141],{"class":100},[56,1981,1982],{"class":66},"\"--outdir\"",[56,1984,141],{"class":100},[56,1986,1987],{"class":66},"\"calc\"",[56,1989,141],{"class":100},[56,1991,1952],{"class":66},[56,1993,523],{"class":100},[56,1995,1996],{"class":508},"check",[56,1998,123],{"class":96},[56,2000,2001],{"class":217},"True",[56,2003,141],{"class":100},[56,2005,2006],{"class":508},"capture_output",[56,2008,123],{"class":96},[56,2010,2001],{"class":217},[56,2012,554],{"class":100},[56,2014,2016,2019,2021,2024,2027,2029,2032,2034,2036,2039,2042],{"class":58,"line":2015},21,[56,2017,2018],{"class":100},"calc ",[56,2020,123],{"class":96},[56,2022,2023],{"class":100}," load_workbook(",[56,2025,2026],{"class":66},"\"calc\u002Flookup_check.xlsx\"",[56,2028,141],{"class":100},[56,2030,2031],{"class":508},"data_only",[56,2033,123],{"class":96},[56,2035,2001],{"class":217},[56,2037,2038],{"class":100},")[",[56,2040,2041],{"class":66},"\"Orders\"",[56,2043,2044],{"class":100},"]\n",[56,2046,2048,2051,2053,2056,2058,2061,2063,2066,2068,2070,2072,2075,2078,2080,2082,2084,2087,2089,2091,2094,2096,2098,2100,2102,2104,2106,2109,2111,2114],{"class":58,"line":2047},22,[56,2049,2050],{"class":100},"excel ",[56,2052,123],{"class":96},[56,2054,2055],{"class":100}," pd.DataFrame([[c.value ",[56,2057,1634],{"class":96},[56,2059,2060],{"class":100}," c ",[56,2062,1640],{"class":96},[56,2064,2065],{"class":100}," row] ",[56,2067,1634],{"class":96},[56,2069,1637],{"class":100},[56,2071,1640],{"class":96},[56,2073,2074],{"class":100}," calc.iter_rows(",[56,2076,2077],{"class":508},"min_row",[56,2079,123],{"class":96},[56,2081,1201],{"class":217},[56,2083,141],{"class":100},[56,2085,2086],{"class":508},"max_col",[56,2088,123],{"class":96},[56,2090,1320],{"class":217},[56,2092,2093],{"class":100},")], ",[56,2095,1425],{"class":508},[56,2097,123],{"class":96},[56,2099,1626],{"class":217},[56,2101,1672],{"class":100},[56,2103,1675],{"class":96},[56,2105,1678],{"class":100},[56,2107,2108],{"class":66},"\"Base\"",[56,2110,141],{"class":100},[56,2112,2113],{"class":66},"\"Tier\"",[56,2115,1689],{"class":100},[56,2117,2119,2122,2124,2127,2130,2132,2134,2136,2139,2142,2144,2147,2149,2152,2154,2156,2158,2160,2162],{"class":58,"line":2118},23,[56,2120,2121],{"class":100},"compare ",[56,2123,123],{"class":96},[56,2125,2126],{"class":100}," excel.assign(",[56,2128,2129],{"class":508},"pandas_base",[56,2131,123],{"class":96},[56,2133,560],{"class":100},[56,2135,491],{"class":66},[56,2137,2138],{"class":100},"].values, ",[56,2140,2141],{"class":508},"pandas_tier",[56,2143,123],{"class":96},[56,2145,2146],{"class":100},"tiered.set_index(",[56,2148,1032],{"class":66},[56,2150,2151],{"class":100},").loc[excel[",[56,2153,1032],{"class":66},[56,2155,523],{"class":100},[56,2157,491],{"class":66},[56,2159,597],{"class":100},[56,2161,600],{"class":217},[56,2163,2164],{"class":100},").values)\n",[56,2166,2168,2170,2173,2175,2177,2179,2181,2184,2186,2188,2190,2193,2196,2198,2200,2202],{"class":58,"line":2167},24,[56,2169,613],{"class":217},[56,2171,2172],{"class":100},"(compare[[",[56,2174,1032],{"class":66},[56,2176,141],{"class":100},[56,2178,2108],{"class":66},[56,2180,141],{"class":100},[56,2182,2183],{"class":66},"\"pandas_base\"",[56,2185,141],{"class":100},[56,2187,2113],{"class":66},[56,2189,141],{"class":100},[56,2191,2192],{"class":66},"\"pandas_tier\"",[56,2194,2195],{"class":100},"]].to_string(",[56,2197,619],{"class":508},[56,2199,123],{"class":96},[56,2201,624],{"class":217},[56,2203,627],{"class":100},[56,2205,2207,2209,2211,2214,2217,2219,2221,2223,2226,2228,2231,2234,2237,2239,2241,2243,2245,2247],{"class":58,"line":2206},25,[56,2208,613],{"class":217},[56,2210,771],{"class":100},[56,2212,2213],{"class":66},"\"all equal:\"",[56,2215,2216],{"class":100},", (compare[",[56,2218,2108],{"class":66},[56,2220,469],{"class":100},[56,2222,472],{"class":96},[56,2224,2225],{"class":100}," compare[",[56,2227,2183],{"class":66},[56,2229,2230],{"class":100},"]).all() ",[56,2232,2233],{"class":96},"and",[56,2235,2236],{"class":100}," (compare[",[56,2238,2113],{"class":66},[56,2240,469],{"class":100},[56,2242,472],{"class":96},[56,2244,2225],{"class":100},[56,2246,2192],{"class":66},[56,2248,2249],{"class":100},"]).all())\n",[10,2251,2252,2253,2256,2257,2260,2261,2263,2264,2267,2268,2272],{},"The base-price formula is the classic multi-criteria XLOOKUP: multiplying the three conditions gives 1 only where all are true, and XLOOKUP looks for that 1. The tier formula changes the quantity condition to ",[27,2254,2255],{},"\u003C="," and sets the ",[14,2258,2259],{},"search mode"," to −1, searching from the bottom so that the last qualifying row — the highest break — is found; this relies on the price list being sorted by minimum quantity, just as ",[27,2262,945],{}," does. Openpyxl needs the ",[27,2265,2266],{},"_xlfn."," prefix for XLOOKUP, as covered in ",[36,2269,2271],{"href":2270},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-name-error-for-new-excel-functions-written-by-python\u002F","Fix #NAME? Errors for New Excel Functions Written by Python",". Every order gets the same base and tier price from the workbook as from pandas — including SO-2, where the base lookup returns the South price of 0.48 and the tiered lookup the 500+ break of 0.41, and the two unpriced orders, which both methods return as 0 through the not-found argument.",[42,2274,2276],{"id":2275},"common-pitfalls","Common pitfalls",[2278,2279,2280,2292,2298,2304,2312],"ul",{},[2281,2282,2283,2287,2288,2291],"li",{},[2284,2285,2286],"strong",{},"Concatenated helper keys."," ",[27,2289,2290],{},"\"Bolts|North\""," breaks when a value contains the separator; merge on the columns themselves.",[2281,2293,2294,2297],{},[2284,2295,2296],{},"No duplicate check."," Duplicate keys give silent first-match results in Excel and doubled order rows in pandas.",[2281,2299,2300,2303],{},[2284,2301,2302],{},"Defaulting to 0 without a flag."," Record which rows were actually found in the lookup table.",[2281,2305,2306,2287,2309,2311],{},[2284,2307,2308],{},"Unsorted data for tiered matches.",[27,2310,945],{}," and search mode −1 both need the price breaks sorted by quantity.",[2281,2313,2314,2320],{},[2284,2315,2316,2317,2319],{},"Missing ",[27,2318,949],{}," in merge_asof."," Without it, price breaks from other products and regions are used.",[42,2322,2324],{"id":2323},"conclusion","Conclusion",[10,2326,2327,2328,946,2331,2333,2334,946,2336,2338,2339,2342],{},"A multi-criteria XLOOKUP is a merge on several columns: ",[27,2329,2330],{},"merge(on=[...], how=\"left\", validate=\"many_to_one\")",[27,2332,644],{}," for the not-found value and a flag for which rows matched. Tiered \"exact or next smaller\" lookups are ",[27,2335,945],{},[27,2337,949],{}," for the exact-match keys and ",[27,2340,2341],{},"direction=\"backward\""," on the quantity, both sides sorted. Validate against the Excel formulas in a recalculated workbook when the two must agree.",[42,2344,2346],{"id":2345},"frequently-asked-questions","Frequently asked questions",[10,2348,2349,2352,2353,2356],{},[2284,2350,2351],{},"How do I do an XLOOKUP with two criteria in pandas?","\nMerge the two DataFrames on both key columns with how=\"left\", for example orders.merge(prices, on=",[56,2354,2355],{},"\"Product\", \"Region\"",", how=\"left\").",[10,2358,2359,2362],{},[2284,2360,2361],{},"How do I return a default value when there is no match?","\nFill the missing values after the merge with fillna, and keep a separate flag for rows that matched so defaults can be distinguished from real values.",[10,2364,2365,2368],{},[2284,2366,2367],{},"How do I look up the next smaller value within groups?","\nUse pd.merge_asof with by set to the exact-match columns, the numeric column as left_on and right_on, direction=\"backward\", and both frames sorted on that column.",[10,2370,2371,2374],{},[2284,2372,2373],{},"Why does my multi-criteria lookup duplicate rows?","\nThe lookup table has duplicate keys; pass validate=\"many_to_one\" to raise an error and fix the duplicates at source.",[42,2376,2378],{"id":2377},"related","Related",[2278,2380,2381,2387,2394,2401],{},[2281,2382,2383,2384,2386],{},"Up one level: ",[36,2385,39],{"href":38}," — Excel functions in Python.",[2281,2388,2389,2393],{},[36,2390,2392],{"href":2391},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-equivalent-in-pandas\u002F","XLOOKUP Equivalent in pandas"," — the single-key case.",[2281,2395,2396,2400],{},[36,2397,2399],{"href":2398},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-excel-data-on-multiple-key-columns\u002F","Merge Excel Data on Multiple Key Columns"," — multi-key merges in general.",[2281,2402,2403,2407],{},[36,2404,2406],{"href":2405},"\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"," — merge_asof on dates.",[2409,2410,2411],"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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":52,"searchDepth":110,"depth":110,"links":2413},[2414,2415,2416,2417,2418,2420,2421,2422,2423,2424],{"id":44,"depth":110,"text":45},{"id":83,"depth":110,"text":84},{"id":659,"depth":110,"text":660},{"id":934,"depth":110,"text":935},{"id":1373,"depth":110,"text":2419},"Returning several columns, and why not apply",{"id":1540,"depth":110,"text":1541},{"id":2275,"depth":110,"text":2276},{"id":2323,"depth":110,"text":2324},{"id":2345,"depth":110,"text":2346},{"id":2377,"depth":110,"text":2378},"2026-10-02","Reproduce Excel lookups that match on two or more columns — XLOOKUP(1, (A=x)*(B=y), C) and its concatenated-key cousins — in pandas: an exact match on several keys with merge, a not-found default, guarding against duplicate keys, tiered \"next smaller\" matches by quantity with merge_asof, last-match lookups, and a check against the same XLOOKUP formulas recalculated in a workbook.","md",[2429,2431,2433,2435],{"q":2351,"a":2430},"Merge the two DataFrames on both key columns with how=\"left\", for example orders.merge(prices, on=[\"Product\", \"Region\"], how=\"left\").",{"q":2361,"a":2432},"Fill the missing values after the merge with fillna, and keep a separate flag for rows that matched so defaults can be distinguished from real values.",{"q":2367,"a":2434},"Use pd.merge_asof with by set to the exact-match columns, the numeric column as left_on and right_on, direction=\"backward\", and both frames sorted on that column.",{"q":2373,"a":2436},"The lookup table has duplicate keys; pass validate=\"many_to_one\" to raise an error and fix the duplicates at source.","xlookup multiple criteria pandas",null,{"breadcrumb":2440},[2441,2444,2447],{"name":2442,"item":2443},"Home","\u002F",{"name":2445,"item":2446},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":39,"item":38},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-with-multiple-criteria-in-pandas",{"title":2450,"description":2451},"XLOOKUP with Multiple Criteria in pandas (Excel Equivalent)","Multi-criteria XLOOKUP in pandas: merge on several columns, if_not_found defaults, validate many_to_one, tiered price lookups with merge_asof by group, last-match search mode, and verifying against Excel XLOOKUP formulas.","xlookup-with-multiple-criteria-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-with-multiple-criteria-in-pandas\u002Findex",[2455,2456],"formula-equivalents","joins-and-lookups","how-to","Jpnon1V0xIj1D4bPFjSNgZQwgBWAL5GDbI2Yn949ePo",[2460,2463],{"title":2392,"path":2461,"stem":2462,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-equivalent-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-equivalent-in-pandas\u002Findex",{"title":2464,"path":2465,"stem":2466,"children":-1},"YEARFRAC and DAYS360 Equivalents in Python","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fyearfrac-and-days360-equivalents-in-python","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fyearfrac-and-days360-equivalents-in-python\u002Findex",[2468,2470],{"path":2469,"title":39},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas",{"path":2471,"title":2445},"\u002Fadvanced-data-transformation-and-cleaning",[],[2474,2477,2480,2483,2486,2489],{"path":2475,"title":2476,"kind":2457},"\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":2478,"title":2479,"kind":2457},"\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":2481,"title":2482,"kind":2457},"\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":2484,"title":2485,"kind":2457},"\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":2487,"title":2488,"kind":2457},"\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":2490,"title":2491,"kind":2457},"\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",1790972090766]