[{"data":1,"prerenderedAt":2171},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fapproximate-match-vlookup-in-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fapproximate-match-vlookup-in-pandas":2141,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fapproximate-match-vlookup-in-pandas":2149,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fapproximate-match-vlookup-in-pandas":2153,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fapproximate-match-vlookup-in-pandas":2154},{"id":4,"title":5,"body":6,"dateModified":2106,"datePublished":2106,"description":2107,"extension":2108,"faq":2109,"keyword":2118,"kind":2119,"meta":2120,"navigation":341,"path":2129,"seo":2130,"slug":2133,"stem":2134,"topics":2135,"type":2139,"__hash__":2140},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fapproximate-match-vlookup-in-pandas\u002Findex.md","Approximate-Match VLOOKUP for Tiered Rates in pandas",{"type":7,"value":8,"toc":2092},"minimark",[9,36,235,240,270,280,284,300,512,529,533,549,705,712,839,843,854,1042,1070,1074,1092,1319,1325,1329,1332,1499,1502,1506,1513,1778,1781,1933,1937,1987,1991,2004,2008,2027,2031,2037,2043,2049,2055,2059,2088],[10,11,12,13,17,18,21,22,25,26,31,32,35],"p",{},"Rate tables are everywhere in business spreadsheets: commission by sales band, discount by order size, shipping cost by weight, tax by income bracket. Excel handles them with ",[14,15,16],"code",{},"VLOOKUP(value, table, 2, TRUE)"," — the \"approximate match\" that finds the largest threshold not above the value — or ",[14,19,20],{},"XLOOKUP"," with match mode -1. The pandas equivalent is ",[14,23,24],{},"merge_asof",", a merge that matches each row to the nearest earlier key instead of an exact one. This page, part of ",[27,28,30],"a",{"href":29},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002F","Excel Formula Equivalents in pandas",", reproduces the lookup exactly — verified against ",[14,33,34],{},"VLOOKUP"," recalculated in LibreOffice — and covers the progressive calculation that a lookup cannot do.",[37,38,46,47,46,51,46,55,46,63,46,70,46,79,46,83,46,89,46,93,46,97,46,101,46,106,46,108,46,112,46,114,46,117,46,120,46,124,46,126,46,129,46,131,46,134,46,137,46,141,46,146,46,150,46,152,46,155,46,158,46,162,46,164,46,167,46,169,46,172,46,175,46,179,46,181,46,184,46,186,46,189,46,197,46,202,46,209,46,220,46,226,46,231],"svg",{"viewBox":39,"role":40,"ariaLabelledBy":41,"xmlns":44,"style":45},"172 0 416 228","img",[42,43],"tier-table-t","tier-table-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:416px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[48,49,50],"title",{"id":42},"A commission tier table and the lookup it performs",[52,53,54],"desc",{"id":43},"A tier table with thresholds 0, 10,000, 25,000 and 50,000 and rates of 2, 3, 5 and 7 percent. Sales of 24,000 match the 10,000 row, the largest threshold not above the value, so the rate is 3 percent. Sales of 25,000 match the 25,000 row exactly and get 5 percent.",[56,57],"rect",{"x":58,"y":59,"width":60,"height":61,"fill":62},"172","0","416","228","#ffffff",[64,65,69],"text",{"x":66,"y":67,"style":68},"380.0","31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","=VLOOKUP(24000, tiers, 2, TRUE) → 3%",[56,71],{"x":72,"y":73,"width":74,"height":75,"rx":59,"fill":76,"stroke":77,"style":78},"188.2","48.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[56,80],{"x":81,"y":73,"width":82,"height":75,"rx":59,"fill":76,"stroke":77,"style":78},"220.2","83.1",[64,84,88],{"x":85,"y":86,"style":87},"261.7","65.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[56,90],{"x":91,"y":73,"width":92,"height":75,"rx":59,"fill":76,"stroke":77,"style":78},"303.3","48.6",[64,94,96],{"x":95,"y":86,"style":87},"327.5","B",[56,98],{"x":72,"y":99,"width":74,"height":100,"rx":59,"fill":76,"stroke":77,"style":78},"72.0","28.0",[64,102,105],{"x":103,"y":104,"style":87},"204.2","91.0","1",[56,107],{"x":81,"y":99,"width":82,"height":100,"rx":59,"fill":62,"stroke":77,"style":78},[64,109,111],{"x":85,"y":104,"style":110},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","threshold",[56,113],{"x":91,"y":99,"width":92,"height":100,"rx":59,"fill":62,"stroke":77,"style":78},[64,115,116],{"x":95,"y":104,"style":110},"rate",[56,118],{"x":72,"y":119,"width":74,"height":100,"rx":59,"fill":76,"stroke":77,"style":78},"100.0",[64,121,123],{"x":103,"y":122,"style":87},"119.0","2",[56,125],{"x":81,"y":119,"width":82,"height":100,"rx":59,"fill":62,"stroke":77,"style":78},[64,127,59],{"x":85,"y":122,"style":128},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[56,130],{"x":91,"y":119,"width":92,"height":100,"rx":59,"fill":62,"stroke":77,"style":78},[64,132,133],{"x":95,"y":122,"style":128},"2%",[56,135],{"x":72,"y":136,"width":74,"height":100,"rx":59,"fill":76,"stroke":77,"style":78},"128.0",[64,138,140],{"x":103,"y":139,"style":87},"147.0","3",[56,142],{"x":81,"y":136,"width":82,"height":100,"rx":59,"fill":143,"stroke":144,"style":145},"#d9f4f1","var(--teal,#0f9488)","stroke-width:1.5px",[64,147,149],{"x":85,"y":139,"style":148},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","10,000",[56,151],{"x":91,"y":136,"width":92,"height":100,"rx":59,"fill":143,"stroke":144,"style":145},[64,153,154],{"x":95,"y":139,"style":148},"3%",[56,156],{"x":72,"y":157,"width":74,"height":100,"rx":59,"fill":76,"stroke":77,"style":78},"156.0",[64,159,161],{"x":103,"y":160,"style":87},"175.0","4",[56,163],{"x":81,"y":157,"width":82,"height":100,"rx":59,"fill":62,"stroke":77,"style":78},[64,165,166],{"x":85,"y":160,"style":128},"25,000",[56,168],{"x":91,"y":157,"width":92,"height":100,"rx":59,"fill":62,"stroke":77,"style":78},[64,170,171],{"x":95,"y":160,"style":128},"5%",[56,173],{"x":72,"y":174,"width":74,"height":100,"rx":59,"fill":76,"stroke":77,"style":78},"184.0",[64,176,178],{"x":103,"y":177,"style":87},"203.0","5",[56,180],{"x":81,"y":174,"width":82,"height":100,"rx":59,"fill":62,"stroke":77,"style":78},[64,182,183],{"x":85,"y":177,"style":128},"50,000",[56,185],{"x":91,"y":174,"width":92,"height":100,"rx":59,"fill":62,"stroke":77,"style":78},[64,187,188],{"x":95,"y":177,"style":128},"7%",[190,191],"line",{"x1":192,"y1":193,"x2":194,"y2":193,"stroke":195,"style":196},"359.8","130.0","391.8","var(--brand,#5b5cf0)","stroke-width:2px",[198,199],"polygon",{"points":200,"fill":201},"395.8,130.0 386.8,135.0 386.8,125.0","#5b5cf0",[56,203],{"x":204,"y":205,"width":206,"height":207,"rx":208,"fill":143,"stroke":144,"style":196},"401.8","74.8","170.0","110.5","12",[210,211,213],"g",{"transform":212},"translate(474.8,87.8) scale(1.000)",[214,215],"path",{"d":216,"fill":217,"stroke":218,"style":219},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5","none","var(--teal-ink,#0b6157)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[64,221,225],{"x":222,"y":223,"style":224},"486.8","134.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","24,000 → 3%",[64,227,230],{"x":222,"y":228,"style":229},"153.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","largest threshold",[64,232,234],{"x":222,"y":233,"style":229},"171.2","not above the value",[236,237,239],"h2",{"id":238},"prerequisites","Prerequisites",[241,242,247],"pre",{"className":243,"code":244,"language":245,"meta":246,"style":246},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl numpy\n","bash","",[14,248,249],{"__ignoreMap":246},[250,251,253,257,261,264,267],"span",{"class":190,"line":252},1,[250,254,256],{"class":255},"sMTad","pip",[250,258,260],{"class":259},"srMev"," install",[250,262,263],{"class":259}," pandas",[250,265,266],{"class":259}," openpyxl",[250,268,269],{"class":259}," numpy\n",[10,271,272,275,276,279],{},[14,273,274],{},"tiers.xlsx"," has four commission tiers starting at 0, 10,000, 25,000 and 50,000. ",[14,277,278],{},"sales.xlsx"," has seven reps whose sales sit on and around the thresholds — including a negative figure from a returns-heavy month.",[236,281,283],{"id":282},"step-1-merge_asof-the-approximate-match-merge","Step 1 — merge_asof: the approximate-match merge",[10,285,286,288,289,292,293,295,296,299],{},[14,287,24],{}," matches each row of the left table to the last row of the right table whose key is less than or equal to it — ",[14,290,291],{},"direction=\"backward\"",", which is the default and exactly ",[14,294,34],{},"'s ",[14,297,298],{},"TRUE",":",[241,301,305],{"className":302,"code":303,"language":304,"meta":246,"style":246},"language-python shiki shiki-themes github-light github-dark-high-contrast","import numpy as np\nimport pandas as pd\n\nsales = pd.read_excel(\"sales.xlsx\")\ntiers = pd.read_excel(\"tiers.xlsx\")\n\nsales[\"sales\"] = sales[\"sales\"].astype(float)\ntiers[\"threshold\"] = tiers[\"threshold\"].astype(float)\n\nrated = pd.merge_asof(sales.sort_values(\"sales\"), tiers.sort_values(\"threshold\"),\n                      left_on=\"sales\", right_on=\"threshold\", direction=\"backward\")\nprint(rated.to_string(index=False))\n","python",[14,306,307,323,336,343,361,376,381,409,433,438,459,492],{"__ignoreMap":246},[250,308,309,313,317,320],{"class":190,"line":252},[250,310,312],{"class":311},"s-kum","import",[250,314,316],{"class":315},"skGVy"," numpy ",[250,318,319],{"class":311},"as",[250,321,322],{"class":315}," np\n",[250,324,326,328,331,333],{"class":190,"line":325},2,[250,327,312],{"class":311},[250,329,330],{"class":315}," pandas ",[250,332,319],{"class":311},[250,334,335],{"class":315}," pd\n",[250,337,339],{"class":190,"line":338},3,[250,340,342],{"emptyLinePlaceholder":341},true,"\n",[250,344,346,349,352,355,358],{"class":190,"line":345},4,[250,347,348],{"class":315},"sales ",[250,350,351],{"class":311},"=",[250,353,354],{"class":315}," pd.read_excel(",[250,356,357],{"class":259},"\"sales.xlsx\"",[250,359,360],{"class":315},")\n",[250,362,364,367,369,371,374],{"class":190,"line":363},5,[250,365,366],{"class":315},"tiers ",[250,368,351],{"class":311},[250,370,354],{"class":315},[250,372,373],{"class":259},"\"tiers.xlsx\"",[250,375,360],{"class":315},[250,377,379],{"class":190,"line":378},6,[250,380,342],{"emptyLinePlaceholder":341},[250,382,384,387,390,393,395,398,400,403,407],{"class":190,"line":383},7,[250,385,386],{"class":315},"sales[",[250,388,389],{"class":259},"\"sales\"",[250,391,392],{"class":315},"] ",[250,394,351],{"class":311},[250,396,397],{"class":315}," sales[",[250,399,389],{"class":259},[250,401,402],{"class":315},"].astype(",[250,404,406],{"class":405},"sP0c6","float",[250,408,360],{"class":315},[250,410,412,415,418,420,422,425,427,429,431],{"class":190,"line":411},8,[250,413,414],{"class":315},"tiers[",[250,416,417],{"class":259},"\"threshold\"",[250,419,392],{"class":315},[250,421,351],{"class":311},[250,423,424],{"class":315}," tiers[",[250,426,417],{"class":259},[250,428,402],{"class":315},[250,430,406],{"class":405},[250,432,360],{"class":315},[250,434,436],{"class":190,"line":435},9,[250,437,342],{"emptyLinePlaceholder":341},[250,439,441,444,446,449,451,454,456],{"class":190,"line":440},10,[250,442,443],{"class":315},"rated ",[250,445,351],{"class":311},[250,447,448],{"class":315}," pd.merge_asof(sales.sort_values(",[250,450,389],{"class":259},[250,452,453],{"class":315},"), tiers.sort_values(",[250,455,417],{"class":259},[250,457,458],{"class":315},"),\n",[250,460,462,466,468,470,473,476,478,480,482,485,487,490],{"class":190,"line":461},11,[250,463,465],{"class":464},"sa561","                      left_on",[250,467,351],{"class":311},[250,469,389],{"class":259},[250,471,472],{"class":315},", ",[250,474,475],{"class":464},"right_on",[250,477,351],{"class":311},[250,479,417],{"class":259},[250,481,472],{"class":315},[250,483,484],{"class":464},"direction",[250,486,351],{"class":311},[250,488,489],{"class":259},"\"backward\"",[250,491,360],{"class":315},[250,493,495,498,501,504,506,509],{"class":190,"line":494},12,[250,496,497],{"class":405},"print",[250,499,500],{"class":315},"(rated.to_string(",[250,502,503],{"class":464},"index",[250,505,351],{"class":311},[250,507,508],{"class":405},"False",[250,510,511],{"class":315},"))\n",[10,513,514,515,518,519,521,522,525,526,528],{},"Every rep gets the rate of the tier their sales fall into: 9,999.99 is still in the 2% tier, exactly 10,000 moves into 3%, 60,000 is in the top tier. The two ",[14,516,517],{},"astype(float)"," lines are not decoration. ",[14,520,24],{}," requires both keys to have the same dtype, and the thresholds, read from whole numbers, arrive as integers while the sales arrive as floats — without the conversion, the merge stops with ",[14,523,524],{},"MergeError: incompatible merge keys",". Both inputs must also be sorted by their keys; ",[14,527,24],{}," raises rather than guess if they are not.",[236,530,532],{"id":531},"step-2-values-below-the-first-tier","Step 2 — Values below the first tier",[10,534,535,536,538,539,542,543,538,545,548],{},"The rep with negative sales has no tier: nothing in the table is less than or equal to −500. ",[14,537,24],{}," returns ",[14,540,541],{},"NaN"," for the rate, where ",[14,544,34],{},[14,546,547],{},"#N\u002FA",". Decide what it means — usually that the tier table should start at the lowest possible value — and handle it explicitly:",[241,550,552],{"className":302,"code":551,"language":304,"meta":246,"style":246},"below = rated[\"rate\"].isna()\nprint(\"no tier for:\", rated.loc[below, [\"rep\", \"sales\"]].to_dict(\"records\"))\n\nrated[\"rate\"] = rated[\"rate\"].fillna(0.0)          # no commission on negative sales\nrated[\"commission\"] = (rated[\"sales\"].clip(lower=0) * rated[\"rate\"]).round(2)\nprint(rated[[\"rep\", \"sales\", \"rate\", \"commission\"]].to_string(index=False))\n",[14,553,554,570,598,602,630,673],{"__ignoreMap":246},[250,555,556,559,561,564,567],{"class":190,"line":252},[250,557,558],{"class":315},"below ",[250,560,351],{"class":311},[250,562,563],{"class":315}," rated[",[250,565,566],{"class":259},"\"rate\"",[250,568,569],{"class":315},"].isna()\n",[250,571,572,574,577,580,583,586,588,590,593,596],{"class":190,"line":325},[250,573,497],{"class":405},[250,575,576],{"class":315},"(",[250,578,579],{"class":259},"\"no tier for:\"",[250,581,582],{"class":315},", rated.loc[below, [",[250,584,585],{"class":259},"\"rep\"",[250,587,472],{"class":315},[250,589,389],{"class":259},[250,591,592],{"class":315},"]].to_dict(",[250,594,595],{"class":259},"\"records\"",[250,597,511],{"class":315},[250,599,600],{"class":190,"line":338},[250,601,342],{"emptyLinePlaceholder":341},[250,603,604,607,609,611,613,615,617,620,623,626],{"class":190,"line":345},[250,605,606],{"class":315},"rated[",[250,608,566],{"class":259},[250,610,392],{"class":315},[250,612,351],{"class":311},[250,614,563],{"class":315},[250,616,566],{"class":259},[250,618,619],{"class":315},"].fillna(",[250,621,622],{"class":405},"0.0",[250,624,625],{"class":315},")          ",[250,627,629],{"class":628},"s-wDw","# no commission on negative sales\n",[250,631,632,634,637,639,641,644,646,649,652,654,656,659,662,664,666,669,671],{"class":190,"line":363},[250,633,606],{"class":315},[250,635,636],{"class":259},"\"commission\"",[250,638,392],{"class":315},[250,640,351],{"class":311},[250,642,643],{"class":315}," (rated[",[250,645,389],{"class":259},[250,647,648],{"class":315},"].clip(",[250,650,651],{"class":464},"lower",[250,653,351],{"class":311},[250,655,59],{"class":405},[250,657,658],{"class":315},") ",[250,660,661],{"class":311},"*",[250,663,563],{"class":315},[250,665,566],{"class":259},[250,667,668],{"class":315},"]).round(",[250,670,123],{"class":405},[250,672,360],{"class":315},[250,674,675,677,680,682,684,686,688,690,692,694,697,699,701,703],{"class":190,"line":378},[250,676,497],{"class":405},[250,678,679],{"class":315},"(rated[[",[250,681,585],{"class":259},[250,683,472],{"class":315},[250,685,389],{"class":259},[250,687,472],{"class":315},[250,689,566],{"class":259},[250,691,472],{"class":315},[250,693,636],{"class":259},[250,695,696],{"class":315},"]].to_string(",[250,698,503],{"class":464},[250,700,351],{"class":311},[250,702,508],{"class":405},[250,704,511],{"class":315},[10,706,707,708,711],{},"Filling with a zero rate and clipping negative sales encodes a business rule — no negative commission — rather than hiding a lookup failure. An alternative is to add a first tier at ",[14,709,710],{},"-inf"," to the table, which makes the rule visible in the tier data itself.",[37,713,46,719,46,722,46,725,46,729,46,735,46,741,46,747,46,750,46,754,46,757,46,761,46,765,46,770,46,772,46,776,46,778,46,781,46,783,46,786,46,789,46,793,46,795,46,799,46,801,46,804,46,806,46,814,46,817,46,821,46,823,46,827,46,829,46,832,46,834],{"viewBox":714,"role":40,"ariaLabelledBy":715,"xmlns":44,"style":718},"4 0 752 174",[716,717],"tier-methods-t","tier-methods-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[48,720,721],{"id":716},"Three ways to do an approximate-match lookup in pandas",[52,723,724],{"id":717},"A comparison of merge_asof, searchsorted and pd.cut. merge_asof returns whole tier rows, needs sorted keys of the same dtype, and supports different tier tables per group with the by argument. searchsorted returns a row position and works on arrays. pd.cut with right=False returns a label and needs the thresholds plus an upper edge. All three return missing for values below the first tier.",[56,726],{"x":161,"y":59,"width":727,"height":728,"fill":62},"752","174",[56,730],{"x":731,"y":732,"width":733,"height":734,"rx":59,"fill":62,"stroke":77,"style":78},"20.0","16.0","258.0","40.0",[56,736],{"x":737,"y":732,"width":738,"height":734,"rx":59,"fill":739,"stroke":195,"style":740},"278.0","154.0","#f0f4ff","stroke-width:1.2px",[64,742,746],{"x":743,"y":744,"style":745},"355.0","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Returns",[56,748],{"x":749,"y":732,"width":738,"height":734,"rx":59,"fill":739,"stroke":195,"style":740},"432.0",[64,751,753],{"x":752,"y":744,"style":745},"509.0","Requirements",[56,755],{"x":756,"y":732,"width":738,"height":734,"rx":59,"fill":739,"stroke":195,"style":740},"586.0",[64,758,760],{"x":759,"y":744,"style":745},"663.0","Per-group tiers",[56,762],{"x":731,"y":763,"width":733,"height":764,"rx":59,"fill":76,"stroke":77,"style":78},"56.0","34.0",[64,766,769],{"x":74,"y":767,"style":768},"78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","merge_asof(direction=\"backward\")",[56,771],{"x":737,"y":763,"width":738,"height":764,"rx":59,"fill":62,"stroke":77,"style":78},[64,773,775],{"x":743,"y":774,"style":229},"77.0","whole tier row",[56,777],{"x":749,"y":763,"width":738,"height":764,"rx":59,"fill":62,"stroke":77,"style":78},[64,779,780],{"x":752,"y":774,"style":229},"sorted, same dtype",[56,782],{"x":756,"y":763,"width":738,"height":764,"rx":59,"fill":62,"stroke":77,"style":78},[64,784,785],{"x":759,"y":774,"style":229},"yes, by=",[56,787],{"x":731,"y":788,"width":733,"height":764,"rx":59,"fill":76,"stroke":77,"style":78},"90.0",[64,790,792],{"x":74,"y":791,"style":768},"112.0","np.searchsorted(side=\"right\") - 1",[56,794],{"x":737,"y":788,"width":738,"height":764,"rx":59,"fill":62,"stroke":77,"style":78},[64,796,798],{"x":743,"y":797,"style":229},"111.0","row position",[56,800],{"x":749,"y":788,"width":738,"height":764,"rx":59,"fill":62,"stroke":77,"style":78},[64,802,803],{"x":752,"y":797,"style":229},"sorted thresholds",[56,805],{"x":756,"y":788,"width":738,"height":764,"rx":59,"fill":62,"stroke":77,"style":78},[210,807,809],{"transform":808},"translate(655.0,99.0) scale(0.667)",[214,810],{"d":811,"fill":217,"stroke":812,"style":813},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)","stroke-width:2.70px;stroke-linecap:round;stroke-linejoin:round",[56,815],{"x":731,"y":816,"width":733,"height":764,"rx":59,"fill":76,"stroke":77,"style":78},"124.0",[64,818,820],{"x":74,"y":819,"style":768},"146.0","pd.cut(right=False, labels=rates)",[56,822],{"x":737,"y":816,"width":738,"height":764,"rx":59,"fill":62,"stroke":77,"style":78},[64,824,826],{"x":743,"y":825,"style":229},"145.0","the label",[56,828],{"x":749,"y":816,"width":738,"height":764,"rx":59,"fill":62,"stroke":77,"style":78},[64,830,831],{"x":752,"y":825,"style":229},"edges + inf",[56,833],{"x":756,"y":816,"width":738,"height":764,"rx":59,"fill":62,"stroke":77,"style":78},[210,835,837],{"transform":836},"translate(655.0,133.0) scale(0.667)",[214,838],{"d":811,"fill":217,"stroke":812,"style":813},[236,840,842],{"id":841},"step-3-searchsorted-and-pdcut-as-alternatives","Step 3 — searchsorted and pd.cut as alternatives",[10,844,845,846,849,850,853],{},"When only the rate is needed, two lighter tools give the same answer. ",[14,847,848],{},"np.searchsorted"," finds, for each value, the position at which it would be inserted into the sorted thresholds; subtracting one gives the tier row. ",[14,851,852],{},"pd.cut"," with left-closed intervals labels each value with its tier:",[241,855,857],{"className":302,"code":856,"language":304,"meta":246,"style":246},"thresholds = tiers[\"threshold\"].to_numpy()\npos = np.searchsorted(thresholds, sales[\"sales\"].to_numpy(), side=\"right\") - 1\nsales[\"rate_ss\"] = np.where(pos >= 0, tiers[\"rate\"].to_numpy()[pos.clip(0)], np.nan)\n\nsales[\"rate_cut\"] = pd.cut(sales[\"sales\"], bins=[*thresholds, np.inf], right=False,\n                           labels=tiers[\"rate\"].tolist(), ordered=False).astype(float)\nprint(sales[[\"rep\", \"sales\", \"rate_ss\", \"rate_cut\"]].to_string(index=False))\n",[14,858,859,873,904,937,941,983,1011],{"__ignoreMap":246},[250,860,861,864,866,868,870],{"class":190,"line":252},[250,862,863],{"class":315},"thresholds ",[250,865,351],{"class":311},[250,867,424],{"class":315},[250,869,417],{"class":259},[250,871,872],{"class":315},"].to_numpy()\n",[250,874,875,878,880,883,885,888,891,893,896,898,901],{"class":190,"line":325},[250,876,877],{"class":315},"pos ",[250,879,351],{"class":311},[250,881,882],{"class":315}," np.searchsorted(thresholds, sales[",[250,884,389],{"class":259},[250,886,887],{"class":315},"].to_numpy(), ",[250,889,890],{"class":464},"side",[250,892,351],{"class":311},[250,894,895],{"class":259},"\"right\"",[250,897,658],{"class":315},[250,899,900],{"class":311},"-",[250,902,903],{"class":405}," 1\n",[250,905,906,908,911,913,915,918,921,924,927,929,932,934],{"class":190,"line":338},[250,907,386],{"class":315},[250,909,910],{"class":259},"\"rate_ss\"",[250,912,392],{"class":315},[250,914,351],{"class":311},[250,916,917],{"class":315}," np.where(pos ",[250,919,920],{"class":311},">=",[250,922,923],{"class":405}," 0",[250,925,926],{"class":315},", tiers[",[250,928,566],{"class":259},[250,930,931],{"class":315},"].to_numpy()[pos.clip(",[250,933,59],{"class":405},[250,935,936],{"class":315},")], np.nan)\n",[250,938,939],{"class":190,"line":345},[250,940,342],{"emptyLinePlaceholder":341},[250,942,943,945,948,950,952,955,957,960,963,965,968,970,973,976,978,980],{"class":190,"line":363},[250,944,386],{"class":315},[250,946,947],{"class":259},"\"rate_cut\"",[250,949,392],{"class":315},[250,951,351],{"class":311},[250,953,954],{"class":315}," pd.cut(sales[",[250,956,389],{"class":259},[250,958,959],{"class":315},"], ",[250,961,962],{"class":464},"bins",[250,964,351],{"class":311},[250,966,967],{"class":315},"[",[250,969,661],{"class":311},[250,971,972],{"class":315},"thresholds, np.inf], ",[250,974,975],{"class":464},"right",[250,977,351],{"class":311},[250,979,508],{"class":405},[250,981,982],{"class":315},",\n",[250,984,985,988,990,992,994,997,1000,1002,1004,1007,1009],{"class":190,"line":378},[250,986,987],{"class":464},"                           labels",[250,989,351],{"class":311},[250,991,414],{"class":315},[250,993,566],{"class":259},[250,995,996],{"class":315},"].tolist(), ",[250,998,999],{"class":464},"ordered",[250,1001,351],{"class":311},[250,1003,508],{"class":405},[250,1005,1006],{"class":315},").astype(",[250,1008,406],{"class":405},[250,1010,360],{"class":315},[250,1012,1013,1015,1018,1020,1022,1024,1026,1028,1030,1032,1034,1036,1038,1040],{"class":190,"line":383},[250,1014,497],{"class":405},[250,1016,1017],{"class":315},"(sales[[",[250,1019,585],{"class":259},[250,1021,472],{"class":315},[250,1023,389],{"class":259},[250,1025,472],{"class":315},[250,1027,910],{"class":259},[250,1029,472],{"class":315},[250,1031,947],{"class":259},[250,1033,696],{"class":315},[250,1035,503],{"class":464},[250,1037,351],{"class":311},[250,1039,508],{"class":405},[250,1041,511],{"class":315},[10,1043,1044,1047,1048,1050,1051,1054,1055,1058,1059,1061,1062,1064,1065,1069],{},[14,1045,1046],{},"side=\"right\""," is what makes a value exactly on a threshold belong to that threshold's tier, matching ",[14,1049,34],{},"; ",[14,1052,1053],{},"side=\"left\""," would push 10,000 back into the 2% tier. ",[14,1056,1057],{},"right=False"," does the same job for ",[14,1060,852],{},". Both keep the original row order, which ",[14,1063,24],{}," does not — it returns rows in key order. ",[27,1066,1068],{"href":1067},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbin-values-into-ranges-with-pandas-cut\u002F","Bin Values into Ranges Like Excel FREQUENCY with pd.cut"," explains the interval options.",[236,1071,1073],{"id":1072},"step-4-different-tiers-for-different-groups","Step 4 — Different tiers for different groups",[10,1075,1076,1077,1079,1080,1083,1084,1088,1089,1091],{},"Tier tables often vary by region, product line or year. ",[14,1078,24],{}," handles that with ",[14,1081,1082],{},"by",", which matches on an exact key ",[1085,1086,1087],"em",{},"and"," an approximate one — ",[14,1090,34],{}," would need one table per region or a helper column:",[241,1093,1095],{"className":302,"code":1094,"language":304,"meta":246,"style":246},"regional = pd.DataFrame({\n    \"region\": [\"North\"] * 3 + [\"South\"] * 3,\n    \"threshold\": [0.0, 10000.0, 30000.0, 0.0, 20000.0, 40000.0],\n    \"rate\": [0.02, 0.04, 0.06, 0.025, 0.035, 0.05],\n}).sort_values(\"threshold\")\n\nby_region = pd.merge_asof(sales.sort_values(\"sales\"), regional,\n                          left_on=\"sales\", right_on=\"threshold\", by=\"region\", direction=\"backward\")\nprint(by_region[[\"rep\", \"region\", \"sales\", \"threshold\", \"rate\"]].sort_values(\"rep\").to_string(index=False))\n",[14,1096,1097,1107,1142,1178,1215,1224,1228,1242,1278],{"__ignoreMap":246},[250,1098,1099,1102,1104],{"class":190,"line":252},[250,1100,1101],{"class":315},"regional ",[250,1103,351],{"class":311},[250,1105,1106],{"class":315}," pd.DataFrame({\n",[250,1108,1109,1112,1115,1118,1120,1122,1125,1128,1131,1134,1136,1138,1140],{"class":190,"line":325},[250,1110,1111],{"class":259},"    \"region\"",[250,1113,1114],{"class":315},": [",[250,1116,1117],{"class":259},"\"North\"",[250,1119,392],{"class":315},[250,1121,661],{"class":311},[250,1123,1124],{"class":405}," 3",[250,1126,1127],{"class":311}," +",[250,1129,1130],{"class":315}," [",[250,1132,1133],{"class":259},"\"South\"",[250,1135,392],{"class":315},[250,1137,661],{"class":311},[250,1139,1124],{"class":405},[250,1141,982],{"class":315},[250,1143,1144,1147,1149,1151,1153,1156,1158,1161,1163,1165,1167,1170,1172,1175],{"class":190,"line":338},[250,1145,1146],{"class":259},"    \"threshold\"",[250,1148,1114],{"class":315},[250,1150,622],{"class":405},[250,1152,472],{"class":315},[250,1154,1155],{"class":405},"10000.0",[250,1157,472],{"class":315},[250,1159,1160],{"class":405},"30000.0",[250,1162,472],{"class":315},[250,1164,622],{"class":405},[250,1166,472],{"class":315},[250,1168,1169],{"class":405},"20000.0",[250,1171,472],{"class":315},[250,1173,1174],{"class":405},"40000.0",[250,1176,1177],{"class":315},"],\n",[250,1179,1180,1183,1185,1188,1190,1193,1195,1198,1200,1203,1205,1208,1210,1213],{"class":190,"line":345},[250,1181,1182],{"class":259},"    \"rate\"",[250,1184,1114],{"class":315},[250,1186,1187],{"class":405},"0.02",[250,1189,472],{"class":315},[250,1191,1192],{"class":405},"0.04",[250,1194,472],{"class":315},[250,1196,1197],{"class":405},"0.06",[250,1199,472],{"class":315},[250,1201,1202],{"class":405},"0.025",[250,1204,472],{"class":315},[250,1206,1207],{"class":405},"0.035",[250,1209,472],{"class":315},[250,1211,1212],{"class":405},"0.05",[250,1214,1177],{"class":315},[250,1216,1217,1220,1222],{"class":190,"line":363},[250,1218,1219],{"class":315},"}).sort_values(",[250,1221,417],{"class":259},[250,1223,360],{"class":315},[250,1225,1226],{"class":190,"line":378},[250,1227,342],{"emptyLinePlaceholder":341},[250,1229,1230,1233,1235,1237,1239],{"class":190,"line":383},[250,1231,1232],{"class":315},"by_region ",[250,1234,351],{"class":311},[250,1236,448],{"class":315},[250,1238,389],{"class":259},[250,1240,1241],{"class":315},"), regional,\n",[250,1243,1244,1247,1249,1251,1253,1255,1257,1259,1261,1263,1265,1268,1270,1272,1274,1276],{"class":190,"line":411},[250,1245,1246],{"class":464},"                          left_on",[250,1248,351],{"class":311},[250,1250,389],{"class":259},[250,1252,472],{"class":315},[250,1254,475],{"class":464},[250,1256,351],{"class":311},[250,1258,417],{"class":259},[250,1260,472],{"class":315},[250,1262,1082],{"class":464},[250,1264,351],{"class":311},[250,1266,1267],{"class":259},"\"region\"",[250,1269,472],{"class":315},[250,1271,484],{"class":464},[250,1273,351],{"class":311},[250,1275,489],{"class":259},[250,1277,360],{"class":315},[250,1279,1280,1282,1285,1287,1289,1291,1293,1295,1297,1299,1301,1303,1306,1308,1311,1313,1315,1317],{"class":190,"line":435},[250,1281,497],{"class":405},[250,1283,1284],{"class":315},"(by_region[[",[250,1286,585],{"class":259},[250,1288,472],{"class":315},[250,1290,1267],{"class":259},[250,1292,472],{"class":315},[250,1294,389],{"class":259},[250,1296,472],{"class":315},[250,1298,417],{"class":259},[250,1300,472],{"class":315},[250,1302,566],{"class":259},[250,1304,1305],{"class":315},"]].sort_values(",[250,1307,585],{"class":259},[250,1309,1310],{"class":315},").to_string(",[250,1312,503],{"class":464},[250,1314,351],{"class":311},[250,1316,508],{"class":405},[250,1318,511],{"class":315},[10,1320,1321,1322,1324],{},"Eve and Finn are both in their region's second tier, but different thresholds apply — 10,000 in the North, 20,000 in the South — and Dev's 10,000, which reached the second tier on the company table, is still in the South's first tier. Sort the tier table by the threshold only; ",[14,1323,24],{}," handles the grouping. Keep all tier tables in one sheet with a region column, so a new region is a few new rows rather than a new lookup.",[236,1326,1328],{"id":1327},"check-the-tier-table-before-trusting-it","Check the tier table before trusting it",[10,1330,1331],{},"Tier tables are maintained by hand, and a single mistake — a threshold typed twice, two rows out of order, a rate entered as 5 instead of 0.05 — changes every result without an error. Validate the table each run, before the lookup:",[241,1333,1335],{"className":302,"code":1334,"language":304,"meta":246,"style":246},"def check_tiers(table, key=\"threshold\", value=\"rate\"):\n    problems = []\n    if table[key].duplicated().any():\n        problems.append(\"duplicate thresholds\")\n    if not table[key].is_monotonic_increasing:\n        problems.append(\"thresholds not in ascending order\")\n    if not table[value].between(0, 1).all():\n        problems.append(\"rates outside 0-1 (percent typed as a whole number?)\")\n    return problems\n\nprint(check_tiers(tiers))\nprint(check_tiers(pd.DataFrame({\"threshold\": [0, 25000, 10000], \"rate\": [0.02, 5, 0.03]})))\n",[14,1336,1337,1363,1373,1381,1391,1401,1410,1428,1437,1445,1449,1456],{"__ignoreMap":246},[250,1338,1339,1342,1346,1349,1351,1353,1356,1358,1360],{"class":190,"line":252},[250,1340,1341],{"class":311},"def",[250,1343,1345],{"class":1344},"s_Opv"," check_tiers",[250,1347,1348],{"class":315},"(table, key",[250,1350,351],{"class":311},[250,1352,417],{"class":259},[250,1354,1355],{"class":315},", value",[250,1357,351],{"class":311},[250,1359,566],{"class":259},[250,1361,1362],{"class":315},"):\n",[250,1364,1365,1368,1370],{"class":190,"line":325},[250,1366,1367],{"class":315},"    problems ",[250,1369,351],{"class":311},[250,1371,1372],{"class":315}," []\n",[250,1374,1375,1378],{"class":190,"line":338},[250,1376,1377],{"class":311},"    if",[250,1379,1380],{"class":315}," table[key].duplicated().any():\n",[250,1382,1383,1386,1389],{"class":190,"line":345},[250,1384,1385],{"class":315},"        problems.append(",[250,1387,1388],{"class":259},"\"duplicate thresholds\"",[250,1390,360],{"class":315},[250,1392,1393,1395,1398],{"class":190,"line":363},[250,1394,1377],{"class":311},[250,1396,1397],{"class":311}," not",[250,1399,1400],{"class":315}," table[key].is_monotonic_increasing:\n",[250,1402,1403,1405,1408],{"class":190,"line":378},[250,1404,1385],{"class":315},[250,1406,1407],{"class":259},"\"thresholds not in ascending order\"",[250,1409,360],{"class":315},[250,1411,1412,1414,1416,1419,1421,1423,1425],{"class":190,"line":383},[250,1413,1377],{"class":311},[250,1415,1397],{"class":311},[250,1417,1418],{"class":315}," table[value].between(",[250,1420,59],{"class":405},[250,1422,472],{"class":315},[250,1424,105],{"class":405},[250,1426,1427],{"class":315},").all():\n",[250,1429,1430,1432,1435],{"class":190,"line":411},[250,1431,1385],{"class":315},[250,1433,1434],{"class":259},"\"rates outside 0-1 (percent typed as a whole number?)\"",[250,1436,360],{"class":315},[250,1438,1439,1442],{"class":190,"line":435},[250,1440,1441],{"class":311},"    return",[250,1443,1444],{"class":315}," problems\n",[250,1446,1447],{"class":190,"line":440},[250,1448,342],{"emptyLinePlaceholder":341},[250,1450,1451,1453],{"class":190,"line":461},[250,1452,497],{"class":405},[250,1454,1455],{"class":315},"(check_tiers(tiers))\n",[250,1457,1458,1460,1463,1465,1467,1469,1471,1474,1476,1479,1481,1483,1485,1487,1489,1491,1493,1496],{"class":190,"line":494},[250,1459,497],{"class":405},[250,1461,1462],{"class":315},"(check_tiers(pd.DataFrame({",[250,1464,417],{"class":259},[250,1466,1114],{"class":315},[250,1468,59],{"class":405},[250,1470,472],{"class":315},[250,1472,1473],{"class":405},"25000",[250,1475,472],{"class":315},[250,1477,1478],{"class":405},"10000",[250,1480,959],{"class":315},[250,1482,566],{"class":259},[250,1484,1114],{"class":315},[250,1486,1187],{"class":405},[250,1488,472],{"class":315},[250,1490,178],{"class":405},[250,1492,472],{"class":315},[250,1494,1495],{"class":405},"0.03",[250,1497,1498],{"class":315},"]})))\n",[10,1500,1501],{},"The real table passes; the broken one fails on order and on the rate typed as 5. Sorting the table in code would hide the first problem rather than fix it — a row out of order usually means a typo in a threshold — so fail and let the owner correct the sheet.",[236,1503,1505],{"id":1504},"step-5-progressive-commission-rates-per-band-not-per-total","Step 5 — Progressive commission: rates per band, not per total",[10,1507,1508,1509,1512],{},"A tiered lookup gives one rate to the whole amount: 25,000 in sales earns 5% of 25,000. Many schemes are ",[1085,1510,1511],{},"progressive"," instead — 2% on the first 10,000, 3% on the next 15,000, 5% above that — like income tax. That is not a lookup at all; it is a sum over the bands:",[241,1514,1516],{"className":302,"code":1515,"language":304,"meta":246,"style":246},"def progressive(amount, thresholds, rates):\n    total = 0.0\n    for i, (low, rate) in enumerate(zip(thresholds, rates)):\n        high = thresholds[i + 1] if i + 1 \u003C len(thresholds) else np.inf\n        if amount > low:\n            total += (min(amount, high) - low) * rate\n    return round(total, 2)\n\nt, r = tiers[\"threshold\"].tolist(), tiers[\"rate\"].tolist()\nsales[\"flat_tier\"] = (sales[\"sales\"].clip(lower=0) * sales[\"rate_ss\"].fillna(0)).round(2)\nsales[\"progressive\"] = sales[\"sales\"].map(lambda a: progressive(a, t, r))\nprint(sales[[\"rep\", \"sales\", \"flat_tier\", \"progressive\"]].to_string(index=False))\n",[14,1517,1518,1528,1538,1560,1603,1617,1644,1658,1662,1681,1724,1748],{"__ignoreMap":246},[250,1519,1520,1522,1525],{"class":190,"line":252},[250,1521,1341],{"class":311},[250,1523,1524],{"class":1344}," progressive",[250,1526,1527],{"class":315},"(amount, thresholds, rates):\n",[250,1529,1530,1533,1535],{"class":190,"line":325},[250,1531,1532],{"class":315},"    total ",[250,1534,351],{"class":311},[250,1536,1537],{"class":405}," 0.0\n",[250,1539,1540,1543,1546,1549,1552,1554,1557],{"class":190,"line":338},[250,1541,1542],{"class":311},"    for",[250,1544,1545],{"class":315}," i, (low, rate) ",[250,1547,1548],{"class":311},"in",[250,1550,1551],{"class":405}," enumerate",[250,1553,576],{"class":315},[250,1555,1556],{"class":405},"zip",[250,1558,1559],{"class":315},"(thresholds, rates)):\n",[250,1561,1562,1565,1567,1570,1573,1576,1578,1581,1584,1586,1588,1591,1594,1597,1600],{"class":190,"line":345},[250,1563,1564],{"class":315},"        high ",[250,1566,351],{"class":311},[250,1568,1569],{"class":315}," thresholds[i ",[250,1571,1572],{"class":311},"+",[250,1574,1575],{"class":405}," 1",[250,1577,392],{"class":315},[250,1579,1580],{"class":311},"if",[250,1582,1583],{"class":315}," i ",[250,1585,1572],{"class":311},[250,1587,1575],{"class":405},[250,1589,1590],{"class":311}," \u003C",[250,1592,1593],{"class":405}," len",[250,1595,1596],{"class":315},"(thresholds) ",[250,1598,1599],{"class":311},"else",[250,1601,1602],{"class":315}," np.inf\n",[250,1604,1605,1608,1611,1614],{"class":190,"line":363},[250,1606,1607],{"class":311},"        if",[250,1609,1610],{"class":315}," amount ",[250,1612,1613],{"class":311},">",[250,1615,1616],{"class":315}," low:\n",[250,1618,1619,1622,1625,1628,1631,1634,1636,1639,1641],{"class":190,"line":378},[250,1620,1621],{"class":315},"            total ",[250,1623,1624],{"class":311},"+=",[250,1626,1627],{"class":315}," (",[250,1629,1630],{"class":405},"min",[250,1632,1633],{"class":315},"(amount, high) ",[250,1635,900],{"class":311},[250,1637,1638],{"class":315}," low) ",[250,1640,661],{"class":311},[250,1642,1643],{"class":315}," rate\n",[250,1645,1646,1648,1651,1654,1656],{"class":190,"line":383},[250,1647,1441],{"class":311},[250,1649,1650],{"class":405}," round",[250,1652,1653],{"class":315},"(total, ",[250,1655,123],{"class":405},[250,1657,360],{"class":315},[250,1659,1660],{"class":190,"line":411},[250,1661,342],{"emptyLinePlaceholder":341},[250,1663,1664,1667,1669,1671,1673,1676,1678],{"class":190,"line":435},[250,1665,1666],{"class":315},"t, r ",[250,1668,351],{"class":311},[250,1670,424],{"class":315},[250,1672,417],{"class":259},[250,1674,1675],{"class":315},"].tolist(), tiers[",[250,1677,566],{"class":259},[250,1679,1680],{"class":315},"].tolist()\n",[250,1682,1683,1685,1688,1690,1692,1695,1697,1699,1701,1703,1705,1707,1709,1711,1713,1715,1717,1720,1722],{"class":190,"line":440},[250,1684,386],{"class":315},[250,1686,1687],{"class":259},"\"flat_tier\"",[250,1689,392],{"class":315},[250,1691,351],{"class":311},[250,1693,1694],{"class":315}," (sales[",[250,1696,389],{"class":259},[250,1698,648],{"class":315},[250,1700,651],{"class":464},[250,1702,351],{"class":311},[250,1704,59],{"class":405},[250,1706,658],{"class":315},[250,1708,661],{"class":311},[250,1710,397],{"class":315},[250,1712,910],{"class":259},[250,1714,619],{"class":315},[250,1716,59],{"class":405},[250,1718,1719],{"class":315},")).round(",[250,1721,123],{"class":405},[250,1723,360],{"class":315},[250,1725,1726,1728,1731,1733,1735,1737,1739,1742,1745],{"class":190,"line":461},[250,1727,386],{"class":315},[250,1729,1730],{"class":259},"\"progressive\"",[250,1732,392],{"class":315},[250,1734,351],{"class":311},[250,1736,397],{"class":315},[250,1738,389],{"class":259},[250,1740,1741],{"class":315},"].map(",[250,1743,1744],{"class":311},"lambda",[250,1746,1747],{"class":315}," a: progressive(a, t, r))\n",[250,1749,1750,1752,1754,1756,1758,1760,1762,1764,1766,1768,1770,1772,1774,1776],{"class":190,"line":494},[250,1751,497],{"class":405},[250,1753,1017],{"class":315},[250,1755,585],{"class":259},[250,1757,472],{"class":315},[250,1759,389],{"class":259},[250,1761,472],{"class":315},[250,1763,1687],{"class":259},[250,1765,472],{"class":315},[250,1767,1730],{"class":259},[250,1769,696],{"class":315},[250,1771,503],{"class":464},[250,1773,351],{"class":311},[250,1775,508],{"class":405},[250,1777,511],{"class":315},[10,1779,1780],{},"At 24,000 in sales, the flat tier pays 720 (3% of everything) and the progressive scheme 620 (2% of 10,000 plus 3% of 14,000). The flat scheme also has a cliff: at 25,000, commission jumps from 720 to 1,250 for one extra dollar of sales, which progressive schemes avoid. Which scheme applies is a contract question — check the plan document, not the spreadsheet — and the two must never be mixed up in code.",[37,1782,46,1788,46,1791,46,1794,46,1798,46,1805,46,1811,46,1813,46,1816,46,1821,46,1826,46,1830,46,1834,46,1837,46,1841,46,1844,46,1847,46,1850,46,1854,46,1858,46,1862,46,1866,46,1870,46,1874,46,1878,46,1882,46,1886,46,1890,46,1893,46,1896,46,1899,46,1902,46,1905,46,1908,46,1910,46,1913,46,1915,46,1917,46,1919,46,1922,46,1925,46,1927,46,1930],{"viewBox":1783,"role":40,"ariaLabelledBy":1784,"xmlns":44,"style":1787},"4 0 748 250",[1785,1786],"tier-cliff-t","tier-cliff-d","width:100%;max-width:748px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[48,1789,1790],{"id":1785},"Flat-tier versus progressive commission as sales grow",[52,1792,1793],{"id":1786},"A line chart of commission against sales from 0 to 60,000. The flat-tier line jumps at each threshold, most visibly from 720 at 24,999 to 1,250 at 25,000. The progressive line rises smoothly with no jumps, reaching 2,600 at 60,000 against 4,200 for the flat tier.",[56,1795],{"x":161,"y":59,"width":1796,"height":1797,"fill":62},"748","250",[56,1799],{"x":1800,"y":1801,"width":1802,"height":1802,"rx":140,"fill":1803,"stroke":1804,"style":145},"64.0","18.0","14.0","#fdefd8","var(--gold,#b4740a)",[64,1806,1810],{"x":1807,"y":1808,"style":1809},"84.0","29.5","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:start","flat tier",[56,1812],{"x":157,"y":1801,"width":1802,"height":1802,"rx":140,"fill":143,"stroke":144,"style":145},[64,1814,1511],{"x":1815,"y":1808,"style":1809},"176.0",[190,1817],{"x1":1818,"y1":1819,"x2":1820,"y2":1819,"stroke":77,"style":78},"45.8","212.0","736.0",[64,1822,59],{"x":1823,"y":1824,"style":1825},"26.9","216.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle",[190,1827],{"x1":1818,"y1":1828,"x2":1820,"y2":1828,"stroke":77,"style":1829},"158.7","stroke-width:1px;stroke-dasharray:5 4",[64,1831,1833],{"x":1823,"y":1832,"style":1825},"162.7","2k",[190,1835],{"x1":1818,"y1":1836,"x2":1820,"y2":1836,"stroke":77,"style":1829},"105.3",[64,1838,1840],{"x":1823,"y":1839,"style":1825},"109.3","4k",[190,1842],{"x1":1818,"y1":1843,"x2":1820,"y2":1843,"stroke":77,"style":1829},"52.0",[64,1845,1846],{"x":1823,"y":763,"style":1825},"6k",[190,1848],{"x1":1818,"y1":1819,"x2":1820,"y2":1819,"stroke":1849,"style":145},"var(--muted,#5b6780)",[64,1851,59],{"x":1852,"y":1853,"style":229},"88.9","230.0",[64,1855,1857],{"x":1856,"y":1853,"style":229},"175.2","9,999",[64,1859,1861],{"x":1860,"y":1853,"style":229},"261.5","10k",[64,1863,1865],{"x":1864,"y":1853,"style":229},"347.8","24k",[64,1867,1869],{"x":1868,"y":1853,"style":229},"434.0","25k",[64,1871,1873],{"x":1872,"y":1853,"style":229},"520.3","49k",[64,1875,1877],{"x":1876,"y":1853,"style":229},"606.6","50k",[64,1879,1881],{"x":1880,"y":1853,"style":229},"692.9","60k",[214,1883],{"d":1884,"fill":217,"stroke":1804,"style":1885},"M88.9,212.0 L175.2,206.7 L261.5,204.0 L347.8,192.8 L434.0,178.7 L520.3,146.7 L606.6,118.7 L692.9,100.0","stroke-width:2.5px;stroke-linejoin:round;stroke-linecap:round",[1887,1888],"circle",{"cx":1852,"cy":1819,"r":1889,"fill":62,"stroke":1804,"style":196},"4.0",[1887,1891],{"cx":1856,"cy":1892,"r":1889,"fill":62,"stroke":1804,"style":196},"206.7",[1887,1894],{"cx":1860,"cy":1895,"r":1889,"fill":62,"stroke":1804,"style":196},"204.0",[1887,1897],{"cx":1864,"cy":1898,"r":1889,"fill":62,"stroke":1804,"style":196},"192.8",[1887,1900],{"cx":1868,"cy":1901,"r":1889,"fill":62,"stroke":1804,"style":196},"178.7",[1887,1903],{"cx":1872,"cy":1904,"r":1889,"fill":62,"stroke":1804,"style":196},"146.7",[1887,1906],{"cx":1876,"cy":1907,"r":1889,"fill":62,"stroke":1804,"style":196},"118.7",[1887,1909],{"cx":1880,"cy":119,"r":1889,"fill":62,"stroke":1804,"style":196},[214,1911],{"d":1912,"fill":217,"stroke":144,"style":1885},"M88.9,212.0 L175.2,206.7 L261.5,206.7 L347.8,195.5 L434.0,194.7 L520.3,162.7 L606.6,161.3 L692.9,142.7",[1887,1914],{"cx":1852,"cy":1819,"r":1889,"fill":62,"stroke":144,"style":196},[1887,1916],{"cx":1856,"cy":1892,"r":1889,"fill":62,"stroke":144,"style":196},[1887,1918],{"cx":1860,"cy":1892,"r":1889,"fill":62,"stroke":144,"style":196},[1887,1920],{"cx":1864,"cy":1921,"r":1889,"fill":62,"stroke":144,"style":196},"195.5",[1887,1923],{"cx":1868,"cy":1924,"r":1889,"fill":62,"stroke":144,"style":196},"194.7",[1887,1926],{"cx":1872,"cy":1832,"r":1889,"fill":62,"stroke":144,"style":196},[1887,1928],{"cx":1876,"cy":1929,"r":1889,"fill":62,"stroke":144,"style":196},"161.3",[1887,1931],{"cx":1880,"cy":1932,"r":1889,"fill":62,"stroke":144,"style":196},"142.7",[236,1934,1936],{"id":1935},"common-pitfalls","Common pitfalls",[1938,1939,1940,1951,1964,1973,1981],"ul",{},[1941,1942,1943,1947,1948,1950],"li",{},[1944,1945,1946],"strong",{},"Mismatched key dtypes."," ",[14,1949,24],{}," needs identical types; convert both keys to float.",[1941,1952,1953,1956,1957,1959,1960,1963],{},[1944,1954,1955],{},"Unsorted inputs."," Sort both sides by their keys; ",[14,1958,24],{}," refuses otherwise, as ",[14,1961,1962],{},"VLOOKUP TRUE"," silently misbehaves on an unsorted table.",[1941,1965,1966,1969,1970,1972],{},[1944,1967,1968],{},"Values below the first tier."," They get ",[14,1971,541],{},"; decide the rule and encode it.",[1941,1974,1975,1980],{},[1944,1976,1977,1979],{},[14,1978,1053],{}," in searchsorted."," Values on a threshold then fall into the tier below.",[1941,1982,1983,1986],{},[1944,1984,1985],{},"Confusing flat tiers with progressive bands."," They pay different amounts; follow the plan document.",[236,1988,1990],{"id":1989},"performance-notes","Performance notes",[10,1992,1993,1995,1996,1999,2000,2003],{},[14,1994,24],{}," and ",[14,1997,1998],{},"searchsorted"," are both binary searches in compiled code and handle millions of rows against thousands of tiers in well under a second. The progressive function runs in Python per row; for large volumes, vectorise it by computing each band's contribution with ",[14,2001,2002],{},"clip"," for all rows at once and summing the columns.",[236,2005,2007],{"id":2006},"conclusion","Conclusion",[10,2009,2010,2012,2013,2016,2017,1995,2019,2022,2023,2026],{},[14,2011,16],{}," is ",[14,2014,2015],{},"merge_asof(..., direction=\"backward\")"," in pandas, with both inputs sorted and both keys of the same dtype; ",[14,2018,792],{},[14,2020,2021],{},"pd.cut(right=False)"," give the same answer when only the rate is needed. Decide explicitly what happens below the first tier, use ",[14,2024,2025],{},"by="," for tier tables that vary by group, and compute progressive schemes as a sum over bands rather than as a lookup.",[236,2028,2030],{"id":2029},"frequently-asked-questions","Frequently asked questions",[10,2032,2033,2036],{},[1944,2034,2035],{},"What does VLOOKUP with TRUE actually return?","\nThe value from the row with the largest key that is less than or equal to the lookup value, in a table sorted ascending by key. A value below the first key returns #N\u002FA.",[10,2038,2039,2042],{},[1944,2040,2041],{},"What is the pandas equivalent?","\npd.merge_asof(left, tiers, left_on=\"sales\", right_on=\"threshold\", direction=\"backward\"). Both sides must be sorted by their key and the keys must have the same dtype. np.searchsorted and pd.cut with right=False give the same answer.",[10,2044,2045,2048],{},[1944,2046,2047],{},"Why does merge_asof raise MergeError about dtypes?","\nIt requires both keys to have exactly the same type. Integer thresholds read from one sheet and float sales from another are a common mismatch; convert both to float before merging.",[10,2050,2051,2054],{},[1944,2052,2053],{},"How is a progressive commission different?","\nA tiered lookup applies one rate to the whole amount. A progressive (marginal) scheme applies each rate only to the part of the amount inside its band, like income tax. It needs a sum over the bands, not a lookup.",[236,2056,2058],{"id":2057},"related","Related",[1938,2060,2061,2067,2074,2081],{},[1941,2062,2063,2064,2066],{},"Up one level: ",[27,2065,30],{"href":29}," — every function, translated.",[1941,2068,2069,2073],{},[27,2070,2072],{"href":2071},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fvlookup-equivalent-in-pandas-for-excel-files\u002F","The VLOOKUP Equivalent in pandas for Excel Files"," — exact-match lookups.",[1941,2075,2076,2080],{},[27,2077,2079],{"href":2078},"\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 pandas merge_asof"," — the same merge on dates.",[1941,2082,2083,2087],{},[27,2084,2086],{"href":2085},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-sales-commission-report\u002F","Build a Sales Commission Report with Python and Excel"," — tiers in a complete report.",[2089,2090,2091],"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":246,"searchDepth":325,"depth":325,"links":2093},[2094,2095,2096,2097,2098,2099,2100,2101,2102,2103,2104,2105],{"id":238,"depth":325,"text":239},{"id":282,"depth":325,"text":283},{"id":531,"depth":325,"text":532},{"id":841,"depth":325,"text":842},{"id":1072,"depth":325,"text":1073},{"id":1327,"depth":325,"text":1328},{"id":1504,"depth":325,"text":1505},{"id":1935,"depth":325,"text":1936},{"id":1989,"depth":325,"text":1990},{"id":2006,"depth":325,"text":2007},{"id":2029,"depth":325,"text":2030},{"id":2057,"depth":325,"text":2058},"2026-09-25","Look up commission rates, discount bands or tax brackets the way VLOOKUP(value, table, 2, TRUE) does — the largest threshold not above the value. Use merge_asof, searchsorted or pd.cut, handle values below the first tier, keep the tier table sorted, use different tiers per region, and calculate progressive (marginal) amounts correctly.","md",[2110,2112,2114,2116],{"q":2035,"a":2111},"The value from the row with the largest key that is less than or equal to the lookup value, in a table sorted ascending by key. A value below the first key returns #N\u002FA.",{"q":2041,"a":2113},"pd.merge_asof(left, tiers, left_on=\"sales\", right_on=\"threshold\", direction=\"backward\"). Both sides must be sorted by their key and the keys must have the same dtype. np.searchsorted and pd.cut with right=False give the same answer.",{"q":2047,"a":2115},"It requires both keys to have exactly the same type. Integer thresholds read from one sheet and float sales from another are a common mismatch; convert both to float before merging.",{"q":2053,"a":2117},"A tiered lookup applies one rate to the whole amount. A progressive (marginal) scheme applies each rate only to the part of the amount inside its band, like income tax. It needs a sum over the bands, not a lookup.","vlookup approximate match tiered lookup commission brackets",null,{"breadcrumb":2121},[2122,2125,2128],{"name":2123,"item":2124},"Home","\u002F",{"name":2126,"item":2127},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":30,"item":29},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fapproximate-match-vlookup-in-pandas",{"title":2131,"description":2132},"VLOOKUP TRUE (Approximate Match) in pandas: merge_asof for Tiers","Reproduce Excel VLOOKUP with approximate match in pandas for rate tables and brackets: merge_asof direction backward, searchsorted, pd.cut, values below the lowest tier, per-group tiers with by, and progressive commission.","approximate-match-vlookup-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fapproximate-match-vlookup-in-pandas\u002Findex",[2136,2137,2138],"formula-equivalents","joins-and-lookups","pandas","how-to","AB5rXN2MS7g9Kj72985uKxfCYV4tHoKx6svgCmE_llc",[2142,2145],{"title":30,"path":2143,"stem":2144,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Findex",{"title":2146,"path":2147,"stem":2148,"children":-1},"AVERAGEIFS, MAXIFS and MINIFS Equivalents in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Faverageifs-maxifs-minifs-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Faverageifs-maxifs-minifs-in-pandas\u002Findex",[2150,2151],{"path":2143,"title":30},{"path":2152,"title":2126},"\u002Fadvanced-data-transformation-and-cleaning",[],[2155,2157,2160,2163,2165,2168],{"path":2156,"title":2072,"kind":2139},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fvlookup-equivalent-in-pandas-for-excel-files",{"path":2158,"title":2159,"kind":2139},"\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":2161,"title":2162,"kind":2139},"\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":2164,"title":1068,"kind":2139},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fbin-values-into-ranges-with-pandas-cut",{"path":2166,"title":2167,"kind":2139},"\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":2169,"title":2170,"kind":2139},"\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",1790972086756]