[{"data":1,"prerenderedAt":2093},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Flook-up-all-matches-for-a-key-instead-of-the-first":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Flook-up-all-matches-for-a-key-instead-of-the-first":2059,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Flook-up-all-matches-for-a-key-instead-of-the-first":2068,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Flook-up-all-matches-for-a-key-instead-of-the-first":2073,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Flook-up-all-matches-for-a-key-instead-of-the-first":2074},{"id":4,"title":5,"body":6,"dateModified":2025,"datePublished":2025,"description":2026,"extension":2027,"faq":2028,"keyword":2037,"kind":2038,"meta":2039,"navigation":134,"path":2049,"seo":2050,"slug":2053,"stem":2054,"topics":2055,"type":2057,"__hash__":2058},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Flook-up-all-matches-for-a-key-instead-of-the-first\u002Findex.md","Look Up All Matches for a Key Instead of the First",{"type":7,"value":8,"toc":2012},"minimark",[9,41,46,74,84,88,98,516,551,555,561,768,792,1012,1016,1019,1198,1218,1222,1229,1411,1433,1437,1440,1540,1559,1698,1702,1709,1880,1884,1931,1935,1947,1951,1957,1963,1969,1975,1979,2008],[10,11,12,13,17,18,21,22,26,27,30,31,34,35,40],"p",{},"A customer list needs each customer's order numbers next to their name. ",[14,15,16],"code",{},"VLOOKUP"," and ",[14,19,20],{},"XLOOKUP"," both return the ",[23,24,25],"em",{},"first"," match, so a customer with five orders shows one order number and nothing hints that four are missing. Excel 365 can return all matches with ",[14,28,29],{},"FILTER",", usually wrapped in ",[14,32,33],{},"TEXTJOIN"," to fit them into one cell; older workbooks use helper columns or array formulas. In pandas, \"all matches\" is a natural operation, but it has several useful shapes: every match joined into one text cell, a count and total next to it, just the second or latest match, each match in its own column, or one output row per match. This page builds each shape, starting from the first-match result a spreadsheet gives, and explains when to use which. It belongs to ",[36,37,39],"a",{"href":38},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002F","Merging and Joining Excel DataFrames",".",[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","",[14,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,79,80,83],{},[14,77,78],{},"crm.xlsx"," has a Customers sheet with four customers and an Orders sheet with eight orders. Oakridge Farms has four orders, Café Señor and Zenith two each, and Haddad & Sons none — the cases a lookup has to handle. LibreOffice (",[14,81,82],{},"soffice",") recalculates the spreadsheet formulas in Step 1.",[42,85,87],{"id":86},"step-1-what-the-spreadsheet-lookups-return","Step 1 — What the spreadsheet lookups return",[10,89,90,91,93,94,97],{},"Write a first-match ",[14,92,16],{}," and an all-matches ",[14,95,96],{},"TEXTJOIN(FILTER(...))"," for each customer and recalculate:",[47,99,103],{"className":100,"code":101,"language":102,"meta":52,"style":52},"language-python shiki shiki-themes github-light github-dark-high-contrast","import subprocess\nfrom openpyxl import load_workbook\n\nwb = load_workbook(\"crm.xlsx\")\nws = wb[\"Customers\"]\nws[\"C1\"], ws[\"D1\"] = \"VLOOKUP (first)\", \"TEXTJOIN of FILTER (all)\"\nfor r in range(2, ws.max_row + 1):\n    ws[f\"C{r}\"] = f'=IFERROR(INDEX(Orders!$A$2:$A$9,MATCH(A{r},Orders!$B$2:$B$9,0)),\"\")'\n    ws[f\"D{r}\"] = f'=IFERROR(_xlfn.TEXTJOIN(\", \",TRUE,_xlfn._xlws.FILTER(Orders!$A$2:$A$9,Orders!$B$2:$B$9=A{r})),\"\")'\nwb.save(\"crm_formulas.xlsx\")\nsubprocess.run([\"soffice\", \"--headless\", \"--convert-to\", \"xlsx\", \"--outdir\", \"calc\", \"crm_formulas.xlsx\"], check=True, capture_output=True)\ncalc = load_workbook(\"calc\u002Fcrm_formulas.xlsx\", data_only=True)[\"Customers\"]\nexcel = {calc[f\"A{r}\"].value: (calc[f\"C{r}\"].value, calc[f\"D{r}\"].value) for r in range(2, calc.max_row + 1)}\nfor k, v in excel.items():\n    print(k, v)\n","python",[14,104,105,115,129,136,154,171,200,234,278,314,325,386,415,494,507],{"__ignoreMap":52},[56,106,107,111],{"class":58,"line":59},[56,108,110],{"class":109},"s-kum","import",[56,112,114],{"class":113},"skGVy"," subprocess\n",[56,116,118,121,124,126],{"class":58,"line":117},2,[56,119,120],{"class":109},"from",[56,122,123],{"class":113}," openpyxl ",[56,125,110],{"class":109},[56,127,128],{"class":113}," load_workbook\n",[56,130,132],{"class":58,"line":131},3,[56,133,135],{"emptyLinePlaceholder":134},true,"\n",[56,137,139,142,145,148,151],{"class":58,"line":138},4,[56,140,141],{"class":113},"wb ",[56,143,144],{"class":109},"=",[56,146,147],{"class":113}," load_workbook(",[56,149,150],{"class":66},"\"crm.xlsx\"",[56,152,153],{"class":113},")\n",[56,155,157,160,162,165,168],{"class":58,"line":156},5,[56,158,159],{"class":113},"ws ",[56,161,144],{"class":109},[56,163,164],{"class":113}," wb[",[56,166,167],{"class":66},"\"Customers\"",[56,169,170],{"class":113},"]\n",[56,172,174,177,180,183,186,189,191,194,197],{"class":58,"line":173},6,[56,175,176],{"class":113},"ws[",[56,178,179],{"class":66},"\"C1\"",[56,181,182],{"class":113},"], ws[",[56,184,185],{"class":66},"\"D1\"",[56,187,188],{"class":113},"] ",[56,190,144],{"class":109},[56,192,193],{"class":66}," \"VLOOKUP (first)\"",[56,195,196],{"class":113},", ",[56,198,199],{"class":66},"\"TEXTJOIN of FILTER (all)\"\n",[56,201,203,206,209,212,216,219,222,225,228,231],{"class":58,"line":202},7,[56,204,205],{"class":109},"for",[56,207,208],{"class":113}," r ",[56,210,211],{"class":109},"in",[56,213,215],{"class":214},"sP0c6"," range",[56,217,218],{"class":113},"(",[56,220,221],{"class":214},"2",[56,223,224],{"class":113},", ws.max_row ",[56,226,227],{"class":109},"+",[56,229,230],{"class":214}," 1",[56,232,233],{"class":113},"):\n",[56,235,237,240,243,246,250,253,256,259,261,263,266,269,271,273,275],{"class":58,"line":236},8,[56,238,239],{"class":113},"    ws[",[56,241,242],{"class":109},"f",[56,244,245],{"class":66},"\"C",[56,247,249],{"class":248},"sSjpA","{",[56,251,252],{"class":113},"r",[56,254,255],{"class":248},"}",[56,257,258],{"class":66},"\"",[56,260,188],{"class":113},[56,262,144],{"class":109},[56,264,265],{"class":109}," f",[56,267,268],{"class":66},"'=IFERROR(INDEX(Orders!$A$2:$A$9,MATCH(A",[56,270,249],{"class":248},[56,272,252],{"class":113},[56,274,255],{"class":248},[56,276,277],{"class":66},",Orders!$B$2:$B$9,0)),\"\")'\n",[56,279,281,283,285,288,290,292,294,296,298,300,302,305,307,309,311],{"class":58,"line":280},9,[56,282,239],{"class":113},[56,284,242],{"class":109},[56,286,287],{"class":66},"\"D",[56,289,249],{"class":248},[56,291,252],{"class":113},[56,293,255],{"class":248},[56,295,258],{"class":66},[56,297,188],{"class":113},[56,299,144],{"class":109},[56,301,265],{"class":109},[56,303,304],{"class":66},"'=IFERROR(_xlfn.TEXTJOIN(\", \",TRUE,_xlfn._xlws.FILTER(Orders!$A$2:$A$9,Orders!$B$2:$B$9=A",[56,306,249],{"class":248},[56,308,252],{"class":113},[56,310,255],{"class":248},[56,312,313],{"class":66},")),\"\")'\n",[56,315,317,320,323],{"class":58,"line":316},10,[56,318,319],{"class":113},"wb.save(",[56,321,322],{"class":66},"\"crm_formulas.xlsx\"",[56,324,153],{"class":113},[56,326,328,331,334,336,339,341,344,346,349,351,354,356,359,361,363,366,370,372,375,377,380,382,384],{"class":58,"line":327},11,[56,329,330],{"class":113},"subprocess.run([",[56,332,333],{"class":66},"\"soffice\"",[56,335,196],{"class":113},[56,337,338],{"class":66},"\"--headless\"",[56,340,196],{"class":113},[56,342,343],{"class":66},"\"--convert-to\"",[56,345,196],{"class":113},[56,347,348],{"class":66},"\"xlsx\"",[56,350,196],{"class":113},[56,352,353],{"class":66},"\"--outdir\"",[56,355,196],{"class":113},[56,357,358],{"class":66},"\"calc\"",[56,360,196],{"class":113},[56,362,322],{"class":66},[56,364,365],{"class":113},"], ",[56,367,369],{"class":368},"sa561","check",[56,371,144],{"class":109},[56,373,374],{"class":214},"True",[56,376,196],{"class":113},[56,378,379],{"class":368},"capture_output",[56,381,144],{"class":109},[56,383,374],{"class":214},[56,385,153],{"class":113},[56,387,389,392,394,396,399,401,404,406,408,411,413],{"class":58,"line":388},12,[56,390,391],{"class":113},"calc ",[56,393,144],{"class":109},[56,395,147],{"class":113},[56,397,398],{"class":66},"\"calc\u002Fcrm_formulas.xlsx\"",[56,400,196],{"class":113},[56,402,403],{"class":368},"data_only",[56,405,144],{"class":109},[56,407,374],{"class":214},[56,409,410],{"class":113},")[",[56,412,167],{"class":66},[56,414,170],{"class":113},[56,416,418,421,423,426,428,431,433,435,437,439,442,444,446,448,450,452,454,457,459,461,463,465,467,469,472,474,476,478,480,482,484,487,489,491],{"class":58,"line":417},13,[56,419,420],{"class":113},"excel ",[56,422,144],{"class":109},[56,424,425],{"class":113}," {calc[",[56,427,242],{"class":109},[56,429,430],{"class":66},"\"A",[56,432,249],{"class":248},[56,434,252],{"class":113},[56,436,255],{"class":248},[56,438,258],{"class":66},[56,440,441],{"class":113},"].value: (calc[",[56,443,242],{"class":109},[56,445,245],{"class":66},[56,447,249],{"class":248},[56,449,252],{"class":113},[56,451,255],{"class":248},[56,453,258],{"class":66},[56,455,456],{"class":113},"].value, calc[",[56,458,242],{"class":109},[56,460,287],{"class":66},[56,462,249],{"class":248},[56,464,252],{"class":113},[56,466,255],{"class":248},[56,468,258],{"class":66},[56,470,471],{"class":113},"].value) ",[56,473,205],{"class":109},[56,475,208],{"class":113},[56,477,211],{"class":109},[56,479,215],{"class":214},[56,481,218],{"class":113},[56,483,221],{"class":214},[56,485,486],{"class":113},", calc.max_row ",[56,488,227],{"class":109},[56,490,230],{"class":214},[56,492,493],{"class":113},")}\n",[56,495,497,499,502,504],{"class":58,"line":496},14,[56,498,205],{"class":109},[56,500,501],{"class":113}," k, v ",[56,503,211],{"class":109},[56,505,506],{"class":113}," excel.items():\n",[56,508,510,513],{"class":58,"line":509},15,[56,511,512],{"class":214},"    print",[56,514,515],{"class":113},"(k, v)\n",[10,517,518,519,522,523,525,526,528,529,531,532,535,536,538,539,542,543,546,547,550],{},"The first-match lookup — written here as ",[14,520,521],{},"INDEX\u002FMATCH",", which behaves like ",[14,524,16],{}," but can return a column to the left — gives SO-101 for Oakridge Farms and nothing more. The ",[14,527,96],{}," version lists all four orders. Two file-format details matter when writing these formulas from Python: ",[14,530,33],{}," needs the ",[14,533,534],{},"_xlfn."," prefix and ",[14,537,29],{}," the longer ",[14,540,541],{},"_xlfn._xlws."," prefix, otherwise the spreadsheet shows ",[14,544,545],{},"#NAME?",". Haddad & Sons, with no orders, gets an empty string from the ",[14,548,549],{},"IFERROR"," wrappers.",[42,552,554],{"id":553},"step-2-all-matches-in-one-cell","Step 2 — All matches in one cell",[10,556,557,558,560],{},"The pandas version of ",[14,559,96],{}," groups the matching table by key and joins the values, then maps the result onto the lookup table:",[47,562,564],{"className":100,"code":563,"language":102,"meta":52,"style":52},"import pandas as pd\n\ncustomers = pd.read_excel(\"crm.xlsx\", sheet_name=\"Customers\")\norders = pd.read_excel(\"crm.xlsx\", sheet_name=\"Orders\").sort_values([\"Customer\", \"Date\"])\n\njoined = orders.groupby(\"Customer\")[\"Order\"].agg(\", \".join)\ncustomers[\"Orders\"] = customers[\"Customer\"].map(joined).fillna(\"\")\nprint(customers.to_string(index=False))\nprint(\"matches spreadsheet:\", all(customers.set_index(\"Customer\")[\"Orders\"][k] == (v[1] or \"\") for k, v in excel.items()))\n",[14,565,566,579,583,606,640,644,670,694,713],{"__ignoreMap":52},[56,567,568,570,573,576],{"class":58,"line":59},[56,569,110],{"class":109},[56,571,572],{"class":113}," pandas ",[56,574,575],{"class":109},"as",[56,577,578],{"class":113}," pd\n",[56,580,581],{"class":58,"line":117},[56,582,135],{"emptyLinePlaceholder":134},[56,584,585,588,590,593,595,597,600,602,604],{"class":58,"line":131},[56,586,587],{"class":113},"customers ",[56,589,144],{"class":109},[56,591,592],{"class":113}," pd.read_excel(",[56,594,150],{"class":66},[56,596,196],{"class":113},[56,598,599],{"class":368},"sheet_name",[56,601,144],{"class":109},[56,603,167],{"class":66},[56,605,153],{"class":113},[56,607,608,611,613,615,617,619,621,623,626,629,632,634,637],{"class":58,"line":138},[56,609,610],{"class":113},"orders ",[56,612,144],{"class":109},[56,614,592],{"class":113},[56,616,150],{"class":66},[56,618,196],{"class":113},[56,620,599],{"class":368},[56,622,144],{"class":109},[56,624,625],{"class":66},"\"Orders\"",[56,627,628],{"class":113},").sort_values([",[56,630,631],{"class":66},"\"Customer\"",[56,633,196],{"class":113},[56,635,636],{"class":66},"\"Date\"",[56,638,639],{"class":113},"])\n",[56,641,642],{"class":58,"line":156},[56,643,135],{"emptyLinePlaceholder":134},[56,645,646,649,651,654,656,658,661,664,667],{"class":58,"line":173},[56,647,648],{"class":113},"joined ",[56,650,144],{"class":109},[56,652,653],{"class":113}," orders.groupby(",[56,655,631],{"class":66},[56,657,410],{"class":113},[56,659,660],{"class":66},"\"Order\"",[56,662,663],{"class":113},"].agg(",[56,665,666],{"class":66},"\", \"",[56,668,669],{"class":113},".join)\n",[56,671,672,675,677,679,681,684,686,689,692],{"class":58,"line":202},[56,673,674],{"class":113},"customers[",[56,676,625],{"class":66},[56,678,188],{"class":113},[56,680,144],{"class":109},[56,682,683],{"class":113}," customers[",[56,685,631],{"class":66},[56,687,688],{"class":113},"].map(joined).fillna(",[56,690,691],{"class":66},"\"\"",[56,693,153],{"class":113},[56,695,696,699,702,705,707,710],{"class":58,"line":236},[56,697,698],{"class":214},"print",[56,700,701],{"class":113},"(customers.to_string(",[56,703,704],{"class":368},"index",[56,706,144],{"class":109},[56,708,709],{"class":214},"False",[56,711,712],{"class":113},"))\n",[56,714,715,717,719,722,724,727,730,732,734,736,739,742,745,748,750,753,756,759,761,763,765],{"class":58,"line":280},[56,716,698],{"class":214},[56,718,218],{"class":113},[56,720,721],{"class":66},"\"matches spreadsheet:\"",[56,723,196],{"class":113},[56,725,726],{"class":214},"all",[56,728,729],{"class":113},"(customers.set_index(",[56,731,631],{"class":66},[56,733,410],{"class":113},[56,735,625],{"class":66},[56,737,738],{"class":113},"][k] ",[56,740,741],{"class":109},"==",[56,743,744],{"class":113}," (v[",[56,746,747],{"class":214},"1",[56,749,188],{"class":113},[56,751,752],{"class":109},"or",[56,754,755],{"class":66}," \"\"",[56,757,758],{"class":113},") ",[56,760,205],{"class":109},[56,762,501],{"class":113},[56,764,211],{"class":109},[56,766,767],{"class":113}," excel.items()))\n",[10,769,770,773,774,777,778,781,782,784,785,788,789,791],{},[14,771,772],{},"groupby(...).agg(\", \".join)"," concatenates every order number per customer into one string; ",[14,775,776],{},"map"," looks each customer up in that result, and ",[14,779,780],{},"fillna(\"\")"," gives customers without orders an empty cell, like the ",[14,783,549],{}," in the formula. (When the recalculated workbook is read back, that empty string arrives as ",[14,786,787],{},"None"," — openpyxl does not store empty strings — so the comparison treats the two as equal.) Sorting by date first means the joined list is in date order, which a reader expects — the spreadsheet's ",[14,790,29],{}," returns matches in sheet order, which happens to be the same here. This shape is good for a readable report column; it is a poor shape for further analysis, because the values are now text.",[793,794,802,803,802,807,802,811,802,819,802,828,802,832,802,839,802,843,802,847,802,851,802,855,802,857,802,861,802,863,802,866,802,869,802,872,802,874,802,878,802,880,802,883,802,886,802,890,802,892,802,895,802,897,802,899,802,902,802,906,802,908,802,911,802,913,802,915,802,918,802,922,802,924,802,927,802,929,802,931,802,935,802,940,802,944,802,948,802,951,802,954,802,959,802,962,802,965,802,969,802,973,802,977,802,983,802,987,802,994,802,999,802,1007],"svg",{"viewBox":795,"role":796,"ariaLabelledBy":797,"xmlns":800,"style":801},"37 0 685 220","img",[798,799],"am-join-t","am-join-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:685px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[804,805,806],"title",{"id":798},"First match versus all matches",[808,809,810],"desc",{"id":799},"Oakridge Farms has four orders. VLOOKUP or INDEX\u002FMATCH returns only the first, SO-101. Joining all matches returns SO-101, SO-103, SO-105, SO-108. A customer with no orders gets an empty cell either way.",[812,813],"rect",{"x":814,"y":815,"width":816,"height":817,"fill":818},"37","0","685","220","#ffffff",[812,820],{"x":821,"y":822,"width":823,"height":824,"rx":815,"fill":825,"stroke":826,"style":827},"53.9","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[812,829],{"x":830,"y":822,"width":831,"height":824,"rx":815,"fill":825,"stroke":826,"style":827},"85.9","64.2",[833,834,838],"text",{"x":835,"y":836,"style":837},"118.0","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[812,840],{"x":841,"y":822,"width":842,"height":824,"rx":815,"fill":825,"stroke":826,"style":827},"150.1","77.1",[833,844,846],{"x":845,"y":836,"style":837},"188.7","B",[812,848],{"x":821,"y":849,"width":823,"height":850,"rx":815,"fill":825,"stroke":826,"style":827},"64.0","28.0",[833,852,747],{"x":853,"y":854,"style":837},"69.9","83.0",[812,856],{"x":830,"y":849,"width":831,"height":850,"rx":815,"fill":818,"stroke":826,"style":827},[833,858,860],{"x":835,"y":854,"style":859},"font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Order",[812,862],{"x":841,"y":849,"width":842,"height":850,"rx":815,"fill":818,"stroke":826,"style":827},[833,864,865],{"x":845,"y":854,"style":859},"Customer",[812,867],{"x":821,"y":868,"width":823,"height":850,"rx":815,"fill":825,"stroke":826,"style":827},"92.0",[833,870,221],{"x":853,"y":871,"style":837},"111.0",[812,873],{"x":830,"y":868,"width":831,"height":850,"rx":815,"fill":818,"stroke":826,"style":827},[833,875,877],{"x":835,"y":871,"style":876},"font-size:10.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","SO-101",[812,879],{"x":841,"y":868,"width":842,"height":850,"rx":815,"fill":818,"stroke":826,"style":827},[833,881,882],{"x":845,"y":871,"style":876},"C001",[812,884],{"x":821,"y":885,"width":823,"height":850,"rx":815,"fill":825,"stroke":826,"style":827},"120.0",[833,887,889],{"x":853,"y":888,"style":837},"139.0","3",[812,891],{"x":830,"y":885,"width":831,"height":850,"rx":815,"fill":818,"stroke":826,"style":827},[833,893,894],{"x":835,"y":888,"style":876},"SO-103",[812,896],{"x":841,"y":885,"width":842,"height":850,"rx":815,"fill":818,"stroke":826,"style":827},[833,898,882],{"x":845,"y":888,"style":876},[812,900],{"x":821,"y":901,"width":823,"height":850,"rx":815,"fill":825,"stroke":826,"style":827},"148.0",[833,903,905],{"x":853,"y":904,"style":837},"167.0","4",[812,907],{"x":830,"y":901,"width":831,"height":850,"rx":815,"fill":818,"stroke":826,"style":827},[833,909,910],{"x":835,"y":904,"style":876},"SO-105",[812,912],{"x":841,"y":901,"width":842,"height":850,"rx":815,"fill":818,"stroke":826,"style":827},[833,914,882],{"x":845,"y":904,"style":876},[812,916],{"x":821,"y":917,"width":823,"height":850,"rx":815,"fill":825,"stroke":826,"style":827},"176.0",[833,919,921],{"x":853,"y":920,"style":837},"195.0","5",[812,923],{"x":830,"y":917,"width":831,"height":850,"rx":815,"fill":818,"stroke":826,"style":827},[833,925,926],{"x":835,"y":920,"style":876},"SO-108",[812,928],{"x":841,"y":917,"width":842,"height":850,"rx":815,"fill":818,"stroke":826,"style":827},[833,930,882],{"x":845,"y":920,"style":876},[812,932],{"x":933,"y":822,"width":934,"height":850,"rx":815,"fill":818,"stroke":826,"style":827},"419.5","85.6",[833,936,939],{"x":937,"y":938,"style":859},"462.3","59.0","method",[812,941],{"x":942,"y":822,"width":943,"height":850,"rx":815,"fill":818,"stroke":826,"style":827},"505.1","201.1",[833,945,947],{"x":946,"y":938,"style":859},"605.6","result",[812,949],{"x":933,"y":950,"width":934,"height":850,"rx":815,"fill":818,"stroke":826,"style":827},"68.0",[833,952,16],{"x":937,"y":953,"style":876},"87.0",[812,955],{"x":942,"y":950,"width":943,"height":850,"rx":815,"fill":956,"stroke":957,"style":958},"#fce9e9","var(--danger,#dc2626)","stroke-width:1.5px",[833,960,877],{"x":946,"y":953,"style":961},"font-size:10.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[812,963],{"x":933,"y":964,"width":934,"height":850,"rx":815,"fill":818,"stroke":826,"style":827},"96.0",[833,966,968],{"x":937,"y":967,"style":876},"115.0","all matches",[812,970],{"x":942,"y":964,"width":943,"height":850,"rx":815,"fill":971,"stroke":972,"style":958},"#d9f4f1","var(--teal,#0f9488)",[833,974,976],{"x":946,"y":967,"style":975},"font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","SO-101, SO-103, SO-105, SO-108",[833,978,982],{"x":979,"y":980,"style":981},"140.6","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","orders",[833,984,986],{"x":985,"y":980,"style":981},"562.8","lookup result",[58,988],{"x1":989,"y1":990,"x2":991,"y2":990,"stroke":992,"style":993},"237.3","82.0","405.5","var(--brand,#5b5cf0)","stroke-width:2px",[995,996],"polygon",{"points":997,"fill":998},"409.5,82.0 400.5,87.0 400.5,77.0","#5b5cf0",[812,1000],{"x":1001,"y":1002,"width":1003,"height":1004,"rx":1005,"fill":818,"stroke":826,"style":1006},"262.5","49.0","121.8","22.0","11","stroke-width:1.2px",[833,1008,1011],{"x":1009,"y":849,"style":1010},"323.4","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","groupby + join",[42,1013,1015],{"id":1014},"step-3-counts-totals-and-the-latest-match","Step 3 — Counts, totals and the latest match",[10,1017,1018],{},"Often what is really wanted is not the list itself but facts about it: how many matches, their total, the most recent one. Named aggregation produces them all in one pass:",[47,1020,1022],{"className":100,"code":1021,"language":102,"meta":52,"style":52},"summary = orders.groupby(\"Customer\").agg(\n    order_count=(\"Order\", \"size\"),\n    total=(\"Amount\", \"sum\"),\n    first_order=(\"Date\", \"min\"),\n    latest_order=(\"Order\", \"last\"),\n)\ncustomers = customers.merge(summary, on=\"Customer\", how=\"left\")\ncustomers[\"order_count\"] = customers[\"order_count\"].fillna(0).astype(int)\nprint(customers.drop(columns=\"Name\").to_string(index=False))\n",[14,1023,1024,1038,1057,1076,1094,1112,1116,1144,1172],{"__ignoreMap":52},[56,1025,1026,1029,1031,1033,1035],{"class":58,"line":59},[56,1027,1028],{"class":113},"summary ",[56,1030,144],{"class":109},[56,1032,653],{"class":113},[56,1034,631],{"class":66},[56,1036,1037],{"class":113},").agg(\n",[56,1039,1040,1043,1045,1047,1049,1051,1054],{"class":58,"line":117},[56,1041,1042],{"class":368},"    order_count",[56,1044,144],{"class":109},[56,1046,218],{"class":113},[56,1048,660],{"class":66},[56,1050,196],{"class":113},[56,1052,1053],{"class":66},"\"size\"",[56,1055,1056],{"class":113},"),\n",[56,1058,1059,1062,1064,1066,1069,1071,1074],{"class":58,"line":131},[56,1060,1061],{"class":368},"    total",[56,1063,144],{"class":109},[56,1065,218],{"class":113},[56,1067,1068],{"class":66},"\"Amount\"",[56,1070,196],{"class":113},[56,1072,1073],{"class":66},"\"sum\"",[56,1075,1056],{"class":113},[56,1077,1078,1081,1083,1085,1087,1089,1092],{"class":58,"line":138},[56,1079,1080],{"class":368},"    first_order",[56,1082,144],{"class":109},[56,1084,218],{"class":113},[56,1086,636],{"class":66},[56,1088,196],{"class":113},[56,1090,1091],{"class":66},"\"min\"",[56,1093,1056],{"class":113},[56,1095,1096,1099,1101,1103,1105,1107,1110],{"class":58,"line":156},[56,1097,1098],{"class":368},"    latest_order",[56,1100,144],{"class":109},[56,1102,218],{"class":113},[56,1104,660],{"class":66},[56,1106,196],{"class":113},[56,1108,1109],{"class":66},"\"last\"",[56,1111,1056],{"class":113},[56,1113,1114],{"class":58,"line":173},[56,1115,153],{"class":113},[56,1117,1118,1120,1122,1125,1128,1130,1132,1134,1137,1139,1142],{"class":58,"line":202},[56,1119,587],{"class":113},[56,1121,144],{"class":109},[56,1123,1124],{"class":113}," customers.merge(summary, ",[56,1126,1127],{"class":368},"on",[56,1129,144],{"class":109},[56,1131,631],{"class":66},[56,1133,196],{"class":113},[56,1135,1136],{"class":368},"how",[56,1138,144],{"class":109},[56,1140,1141],{"class":66},"\"left\"",[56,1143,153],{"class":113},[56,1145,1146,1148,1151,1153,1155,1157,1159,1162,1164,1167,1170],{"class":58,"line":236},[56,1147,674],{"class":113},[56,1149,1150],{"class":66},"\"order_count\"",[56,1152,188],{"class":113},[56,1154,144],{"class":109},[56,1156,683],{"class":113},[56,1158,1150],{"class":66},[56,1160,1161],{"class":113},"].fillna(",[56,1163,815],{"class":214},[56,1165,1166],{"class":113},").astype(",[56,1168,1169],{"class":214},"int",[56,1171,153],{"class":113},[56,1173,1174,1176,1179,1182,1184,1187,1190,1192,1194,1196],{"class":58,"line":280},[56,1175,698],{"class":214},[56,1177,1178],{"class":113},"(customers.drop(",[56,1180,1181],{"class":368},"columns",[56,1183,144],{"class":109},[56,1185,1186],{"class":66},"\"Name\"",[56,1188,1189],{"class":113},").to_string(",[56,1191,704],{"class":368},[56,1193,144],{"class":109},[56,1195,709],{"class":214},[56,1197,712],{"class":113},[10,1199,1200,1201,196,1204,196,1207,1210,1211,1213,1214,40],{},"Each customer row now carries its number of orders — four for Oakridge Farms, totalling 3,230 — their total value, the date of the first one and the latest order number (the last after sorting by date). A left merge keeps every customer, and customers without orders get zero orders and missing values elsewhere — the honest result, rather than a zero total that could be confused with orders worth nothing. In a spreadsheet this would be ",[14,1202,1203],{},"COUNTIFS",[14,1205,1206],{},"SUMIFS",[14,1208,1209],{},"MINIFS"," and an ",[14,1212,20],{}," searching from the end; see ",[36,1215,1217],{"href":1216},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Faverageifs-maxifs-minifs-in-pandas\u002F","AVERAGEIFS, MAXIFS and MINIFS Equivalents in pandas",[42,1219,1221],{"id":1220},"step-4-the-nth-match-or-each-match-in-its-own-column","Step 4 — The nth match, or each match in its own column",[10,1223,1224,1225,1228],{},"To pick the second match, or to spread matches across columns, number the matches within each key with ",[14,1226,1227],{},"cumcount",":",[47,1230,1232],{"className":100,"code":1231,"language":102,"meta":52,"style":52},"orders[\"n\"] = orders.groupby(\"Customer\").cumcount() + 1\nsecond = orders.loc[orders[\"n\"] == 2].set_index(\"Customer\")[\"Order\"]\nprint(\"second order:\", dict(zip(customers[\"Customer\"], customers[\"Customer\"].map(second))))\n\nwide = orders.pivot(index=\"Customer\", columns=\"n\", values=\"Order\").add_prefix(\"Order \")\nprint(customers[[\"Customer\"]].merge(wide, left_on=\"Customer\", right_index=True, how=\"left\").to_string(index=False))\n",[14,1233,1234,1258,1288,1320,1324,1365],{"__ignoreMap":52},[56,1235,1236,1239,1242,1244,1246,1248,1250,1253,1255],{"class":58,"line":59},[56,1237,1238],{"class":113},"orders[",[56,1240,1241],{"class":66},"\"n\"",[56,1243,188],{"class":113},[56,1245,144],{"class":109},[56,1247,653],{"class":113},[56,1249,631],{"class":66},[56,1251,1252],{"class":113},").cumcount() ",[56,1254,227],{"class":109},[56,1256,1257],{"class":214}," 1\n",[56,1259,1260,1263,1265,1268,1270,1272,1274,1277,1280,1282,1284,1286],{"class":58,"line":117},[56,1261,1262],{"class":113},"second ",[56,1264,144],{"class":109},[56,1266,1267],{"class":113}," orders.loc[orders[",[56,1269,1241],{"class":66},[56,1271,188],{"class":113},[56,1273,741],{"class":109},[56,1275,1276],{"class":214}," 2",[56,1278,1279],{"class":113},"].set_index(",[56,1281,631],{"class":66},[56,1283,410],{"class":113},[56,1285,660],{"class":66},[56,1287,170],{"class":113},[56,1289,1290,1292,1294,1297,1299,1302,1304,1307,1310,1312,1315,1317],{"class":58,"line":131},[56,1291,698],{"class":214},[56,1293,218],{"class":113},[56,1295,1296],{"class":66},"\"second order:\"",[56,1298,196],{"class":113},[56,1300,1301],{"class":214},"dict",[56,1303,218],{"class":113},[56,1305,1306],{"class":214},"zip",[56,1308,1309],{"class":113},"(customers[",[56,1311,631],{"class":66},[56,1313,1314],{"class":113},"], customers[",[56,1316,631],{"class":66},[56,1318,1319],{"class":113},"].map(second))))\n",[56,1321,1322],{"class":58,"line":138},[56,1323,135],{"emptyLinePlaceholder":134},[56,1325,1326,1329,1331,1334,1336,1338,1340,1342,1344,1346,1348,1350,1353,1355,1357,1360,1363],{"class":58,"line":156},[56,1327,1328],{"class":113},"wide ",[56,1330,144],{"class":109},[56,1332,1333],{"class":113}," orders.pivot(",[56,1335,704],{"class":368},[56,1337,144],{"class":109},[56,1339,631],{"class":66},[56,1341,196],{"class":113},[56,1343,1181],{"class":368},[56,1345,144],{"class":109},[56,1347,1241],{"class":66},[56,1349,196],{"class":113},[56,1351,1352],{"class":368},"values",[56,1354,144],{"class":109},[56,1356,660],{"class":66},[56,1358,1359],{"class":113},").add_prefix(",[56,1361,1362],{"class":66},"\"Order \"",[56,1364,153],{"class":113},[56,1366,1367,1369,1372,1374,1377,1380,1382,1384,1386,1389,1391,1393,1395,1397,1399,1401,1403,1405,1407,1409],{"class":58,"line":173},[56,1368,698],{"class":214},[56,1370,1371],{"class":113},"(customers[[",[56,1373,631],{"class":66},[56,1375,1376],{"class":113},"]].merge(wide, ",[56,1378,1379],{"class":368},"left_on",[56,1381,144],{"class":109},[56,1383,631],{"class":66},[56,1385,196],{"class":113},[56,1387,1388],{"class":368},"right_index",[56,1390,144],{"class":109},[56,1392,374],{"class":214},[56,1394,196],{"class":113},[56,1396,1136],{"class":368},[56,1398,144],{"class":109},[56,1400,1141],{"class":66},[56,1402,1189],{"class":113},[56,1404,704],{"class":368},[56,1406,144],{"class":109},[56,1408,709],{"class":214},[56,1410,712],{"class":113},[10,1412,1413,1416,1417,1420,1421,1424,1425,1428,1429,40],{},[14,1414,1415],{},"cumcount() + 1"," numbers each customer's orders 1, 2, 3 … in date order, so \"the second match\" is a filter on ",[14,1418,1419],{},"n == 2"," — the job people use ",[14,1422,1423],{},"INDEX"," with ",[14,1426,1427],{},"SMALL(IF(...))"," array formulas for in older Excel. Pivoting on that number puts each match in its own column — Order 1, Order 2, … — which suits a fixed-width report but becomes unwieldy when one key has dozens of matches. Numbering rows within groups is covered in ",[36,1430,1432],{"href":1431},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcreating-pivot-tables-from-excel-data\u002Fnumber-rows-within-each-group-in-pandas\u002F","Number Rows Within Each Group in pandas",[42,1434,1436],{"id":1435},"step-5-one-row-per-match-the-one-to-many-merge","Step 5 — One row per match: the one-to-many merge",[10,1438,1439],{},"When the goal is analysis rather than a display column, keep one row per match. A plain merge does exactly that:",[47,1441,1443],{"className":100,"code":1442,"language":102,"meta":52,"style":52},"detail = customers[[\"Customer\", \"Name\"]].merge(orders[[\"Customer\", \"Order\", \"Date\", \"Amount\"]], on=\"Customer\", how=\"left\")\nprint(detail.to_string(index=False))\nprint(\"rows:\", len(detail), \"| customers:\", detail[\"Customer\"].nunique())\n",[14,1444,1445,1497,1512],{"__ignoreMap":52},[56,1446,1447,1450,1452,1455,1457,1459,1461,1464,1466,1468,1470,1472,1474,1476,1478,1481,1483,1485,1487,1489,1491,1493,1495],{"class":58,"line":59},[56,1448,1449],{"class":113},"detail ",[56,1451,144],{"class":109},[56,1453,1454],{"class":113}," customers[[",[56,1456,631],{"class":66},[56,1458,196],{"class":113},[56,1460,1186],{"class":66},[56,1462,1463],{"class":113},"]].merge(orders[[",[56,1465,631],{"class":66},[56,1467,196],{"class":113},[56,1469,660],{"class":66},[56,1471,196],{"class":113},[56,1473,636],{"class":66},[56,1475,196],{"class":113},[56,1477,1068],{"class":66},[56,1479,1480],{"class":113},"]], ",[56,1482,1127],{"class":368},[56,1484,144],{"class":109},[56,1486,631],{"class":66},[56,1488,196],{"class":113},[56,1490,1136],{"class":368},[56,1492,144],{"class":109},[56,1494,1141],{"class":66},[56,1496,153],{"class":113},[56,1498,1499,1501,1504,1506,1508,1510],{"class":58,"line":117},[56,1500,698],{"class":214},[56,1502,1503],{"class":113},"(detail.to_string(",[56,1505,704],{"class":368},[56,1507,144],{"class":109},[56,1509,709],{"class":214},[56,1511,712],{"class":113},[56,1513,1514,1516,1518,1521,1523,1526,1529,1532,1535,1537],{"class":58,"line":131},[56,1515,698],{"class":214},[56,1517,218],{"class":113},[56,1519,1520],{"class":66},"\"rows:\"",[56,1522,196],{"class":113},[56,1524,1525],{"class":214},"len",[56,1527,1528],{"class":113},"(detail), ",[56,1530,1531],{"class":66},"\"| customers:\"",[56,1533,1534],{"class":113},", detail[",[56,1536,631],{"class":66},[56,1538,1539],{"class":113},"].nunique())\n",[10,1541,1542,1543,1546,1547,1550,1551,1554,1555,40],{},"The merge repeats each customer once per order, keeping every match as its own row with all its columns — nine rows here: eight orders plus Haddad & Sons with empty order fields, thanks to ",[14,1544,1545],{},"how=\"left\"",". This is the shape for pivots, filters and charts. It is also the one that surprises people who expected a lookup: the row count grows. That growth is correct for a one-to-many relationship, but when a lookup table is ",[23,1548,1549],{},"supposed"," to have one row per key, an unexpected increase means duplicates — which is what ",[14,1552,1553],{},"validate=\"many_to_one\""," catches, as described in ",[36,1556,1558],{"href":1557},"\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",[793,1560,802,1566,802,1569,802,1572,802,1576,802,1581,802,1586,802,1592,802,1595,802,1599,802,1603,802,1608,802,1610,802,1615,802,1617,802,1620,802,1623,802,1627,802,1629,802,1632,802,1634,802,1637,802,1640,802,1644,802,1646,802,1650,802,1652,802,1655,802,1658,802,1662,802,1664,802,1668,802,1670,802,1673,802,1676,802,1680,802,1682,802,1686,802,1688,802,1691,802,1694],{"viewBox":1561,"role":796,"ariaLabelledBy":1562,"xmlns":800,"style":1565},"4 0 1027 242",[1563,1564],"am-shapes-t","am-shapes-d","width:100%;max-width:1027px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[804,1567,1568],{"id":1563},"Five shapes for 'all matches'",[808,1570,1571],{"id":1564},"Joined text in one cell suits a report column. Counts and totals suit a summary. The nth match picks a single value. One column per match suits a fixed-width layout. One row per match, from a merge, suits analysis and pivots.",[812,1573],{"x":905,"y":815,"width":1574,"height":1575,"fill":818},"1027","242",[812,1577],{"x":1578,"y":1579,"width":1580,"height":822,"rx":815,"fill":818,"stroke":826,"style":827},"20.0","16.0","170.8",[812,1582],{"x":1583,"y":1579,"width":1584,"height":822,"rx":815,"fill":1585,"stroke":992,"style":1006},"190.8","274.6","#f0f4ff",[833,1587,1591],{"x":1588,"y":1589,"style":1590},"328.1","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","pandas",[812,1593],{"x":1594,"y":1579,"width":1584,"height":822,"rx":815,"fill":1585,"stroke":992,"style":1006},"465.4",[833,1596,1598],{"x":1597,"y":1589,"style":1590},"602.7","best for",[812,1600],{"x":1578,"y":1601,"width":1580,"height":1602,"rx":815,"fill":825,"stroke":826,"style":827},"56.0","34.0",[833,1604,1607],{"x":823,"y":1605,"style":1606},"78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","joined in one cell",[812,1609],{"x":1583,"y":1601,"width":1584,"height":1602,"rx":815,"fill":818,"stroke":826,"style":827},[833,1611,1614],{"x":1588,"y":1612,"style":1613},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","groupby.agg(', '.join)",[812,1616],{"x":1594,"y":1601,"width":1584,"height":1602,"rx":815,"fill":818,"stroke":826,"style":827},[833,1618,1619],{"x":1597,"y":1612,"style":1613},"report column",[812,1621],{"x":1578,"y":1622,"width":1580,"height":1602,"rx":815,"fill":825,"stroke":826,"style":827},"90.0",[833,1624,1626],{"x":823,"y":1625,"style":1606},"112.0","counts and totals",[812,1628],{"x":1583,"y":1622,"width":1584,"height":1602,"rx":815,"fill":818,"stroke":826,"style":827},[833,1630,1631],{"x":1588,"y":871,"style":1613},"groupby.agg(size, sum…)",[812,1633],{"x":1594,"y":1622,"width":1584,"height":1602,"rx":815,"fill":818,"stroke":826,"style":827},[833,1635,1636],{"x":1597,"y":871,"style":1613},"summaries",[812,1638],{"x":1578,"y":1639,"width":1580,"height":1602,"rx":815,"fill":825,"stroke":826,"style":827},"124.0",[833,1641,1643],{"x":823,"y":1642,"style":1606},"146.0","nth match",[812,1645],{"x":1583,"y":1639,"width":1584,"height":1602,"rx":815,"fill":818,"stroke":826,"style":827},[833,1647,1649],{"x":1588,"y":1648,"style":1613},"145.0","cumcount + filter",[812,1651],{"x":1594,"y":1639,"width":1584,"height":1602,"rx":815,"fill":818,"stroke":826,"style":827},[833,1653,1654],{"x":1597,"y":1648,"style":1613},"second, latest",[812,1656],{"x":1578,"y":1657,"width":1580,"height":1602,"rx":815,"fill":825,"stroke":826,"style":827},"158.0",[833,1659,1661],{"x":823,"y":1660,"style":1606},"180.0","one column per match",[812,1663],{"x":1583,"y":1657,"width":1584,"height":1602,"rx":815,"fill":818,"stroke":826,"style":827},[833,1665,1667],{"x":1588,"y":1666,"style":1613},"179.0","cumcount + pivot",[812,1669],{"x":1594,"y":1657,"width":1584,"height":1602,"rx":815,"fill":818,"stroke":826,"style":827},[833,1671,1672],{"x":1597,"y":1666,"style":1613},"fixed layouts",[812,1674],{"x":1578,"y":1675,"width":1580,"height":1602,"rx":815,"fill":825,"stroke":826,"style":827},"192.0",[833,1677,1679],{"x":823,"y":1678,"style":1606},"214.0","one row per match",[812,1681],{"x":1583,"y":1675,"width":1584,"height":1602,"rx":815,"fill":818,"stroke":826,"style":827},[833,1683,1685],{"x":1588,"y":1684,"style":1613},"213.0","merge (one-to-many)",[812,1687],{"x":1594,"y":1675,"width":1584,"height":1602,"rx":815,"fill":818,"stroke":826,"style":827},[833,1689,1690],{"x":1597,"y":1684,"style":1613},"analysis",[812,1692],{"x":1693,"y":1675,"width":1584,"height":1602,"rx":815,"fill":818,"stroke":826,"style":827},"740.0",[833,1695,1697],{"x":1696,"y":1684,"style":1613},"877.3","pivots",[42,1699,1701],{"id":1700},"which-shape-to-use","Which shape to use",[10,1703,1704,1705,1708],{},"The shape follows from the question. If a person will read the result next to each key, join the matches into text — but keep the list short, or show a count with the first few. If the question is \"how many\" or \"how much\", aggregate and never build the text at all. If one specific match matters — the first order, the latest invoice, the second contact — number the matches and filter. And if anything will be calculated, filtered or pivoted afterwards, keep one row per match and summarise at the end. Building the joined text early and then trying to analyse it is the most common way these workflows go wrong: splitting \"SO-101, SO-103\" back apart to count it undoes work that ",[14,1706,1707],{},"groupby"," would have done directly.",[793,1710,802,1716,802,1719,802,1722,802,1726,802,1732,802,1743,802,1748,802,1752,802,1756,802,1759,802,1762,802,1769,802,1774,802,1779,802,1785,802,1789,802,1793,802,1796,802,1799,802,1803,802,1806,802,1809,802,1816,802,1820,802,1823,802,1826,802,1829,802,1833,802,1836,802,1841,802,1848,802,1851,802,1853,802,1855,802,1858,802,1861,802,1864,802,1867,802,1874,802,1877],{"viewBox":1711,"role":796,"ariaLabelledBy":1712,"xmlns":800,"style":1715},"4 0 752 272",[1713,1714],"am-which-t","am-which-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[804,1717,1718],{"id":1713},"Choosing the shape of an all-matches lookup",[808,1720,1721],{"id":1714},"If a person reads the matches next to each key, join them into one cell. If the question is how many or how much, aggregate counts and totals. If one particular match matters, number the matches and filter. If the result will be analysed further, keep one row per match.",[812,1723],{"x":905,"y":815,"width":1724,"height":1725,"fill":818},"752","272",[812,1727],{"x":1728,"y":1579,"width":1729,"height":1730,"rx":1731,"fill":1585,"stroke":992,"style":993},"240.0","280.0","75.5","12",[1733,1734,1736],"g",{"transform":1735},"translate(368.0,29.0) scale(1.000)",[1737,1738],"path",{"d":1739,"fill":1740,"stroke":1741,"style":1742},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[833,1744,1747],{"x":1745,"y":1730,"style":1746},"380.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","What will happen to the result?",[58,1749],{"x1":1745,"y1":1750,"x2":1745,"y2":1751,"stroke":992,"style":993},"91.5","111.5",[58,1753],{"x1":1754,"y1":1751,"x2":1755,"y2":1751,"stroke":992,"style":993},"104.0","656.0",[58,1757],{"x1":1754,"y1":1751,"x2":1754,"y2":1758,"stroke":992,"style":993},"157.5",[995,1760],{"points":1761,"fill":998},"104.0,161.5 99.0,152.5 109.0,152.5",[812,1763],{"x":1764,"y":1765,"width":1766,"height":1767,"rx":1768,"fill":818,"stroke":826,"style":1006},"31.8","121.5","144.4","21.0","10",[833,1770,1773],{"x":1754,"y":1771,"style":1772},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","read next to each key",[812,1775],{"x":1578,"y":1776,"width":1777,"height":1778,"rx":1731,"fill":1585,"stroke":992,"style":993},"163.5","168.0","93.0",[1733,1780,1782],{"transform":1781},"translate(92.0,176.5) scale(1.000)",[1737,1783],{"d":1784,"fill":1740,"stroke":1741,"style":1742},"M6 3h9l4 4v14H6z M15 3v4h4",[833,1786,1788],{"x":1754,"y":1787,"style":1746},"223.0","join into one cell",[833,1790,1792],{"x":1754,"y":1791,"style":1613},"242.5","SO-101, SO-103, …",[58,1794],{"x1":1795,"y1":1751,"x2":1795,"y2":1758,"stroke":992,"style":993},"288.0",[995,1797],{"points":1798,"fill":998},"288.0,161.5 283.0,152.5 293.0,152.5",[812,1800],{"x":1801,"y":1765,"width":1802,"height":1767,"rx":1768,"fill":818,"stroke":826,"style":1006},"212.7","150.5",[833,1804,1805],{"x":1795,"y":1771,"style":1772},"how many \u002F how much",[812,1807],{"x":1808,"y":1776,"width":1777,"height":1778,"rx":1731,"fill":971,"stroke":972,"style":993},"204.0",[1733,1810,1812],{"transform":1811},"translate(276.0,176.5) scale(1.000)",[1737,1813],{"d":1814,"fill":1740,"stroke":1815,"style":1742},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16","var(--teal-ink,#0b6157)",[833,1817,1819],{"x":1795,"y":1787,"style":1818},"font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","aggregate",[833,1821,1822],{"x":1795,"y":1791,"style":1613},"count, total, latest",[58,1824],{"x1":1825,"y1":1751,"x2":1825,"y2":1758,"stroke":992,"style":993},"472.0",[995,1827],{"points":1828,"fill":998},"472.0,161.5 467.0,152.5 477.0,152.5",[812,1830],{"x":1831,"y":1765,"width":1832,"height":1767,"rx":1768,"fill":818,"stroke":826,"style":1006},"402.0","140.0",[833,1834,1835],{"x":1825,"y":1771,"style":1772},"one particular match",[812,1837],{"x":1838,"y":1776,"width":1777,"height":1778,"rx":1731,"fill":1839,"stroke":1840,"style":993},"388.0","#fdefd8","var(--gold,#b4740a)",[1733,1842,1844],{"transform":1843},"translate(460.0,176.5) scale(1.000)",[1737,1845],{"d":1846,"fill":1740,"stroke":1847,"style":1742},"M3 5h18l-7 8v6l-4 2v-8z","var(--gold-ink,#7a4e06)",[833,1849,1649],{"x":1825,"y":1787,"style":1850},"font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[833,1852,1654],{"x":1825,"y":1791,"style":1613},[58,1854],{"x1":1755,"y1":1751,"x2":1755,"y2":1758,"stroke":992,"style":993},[995,1856],{"points":1857,"fill":998},"656.0,161.5 651.0,152.5 661.0,152.5",[812,1859],{"x":1860,"y":1765,"width":871,"height":1767,"rx":1768,"fill":818,"stroke":826,"style":1006},"600.5",[833,1862,1863],{"x":1755,"y":1771,"style":1772},"further analysis",[812,1865],{"x":1866,"y":1776,"width":1777,"height":1778,"rx":1731,"fill":825,"stroke":826,"style":993},"572.0",[1733,1868,1870],{"transform":1869},"translate(644.0,176.5) scale(1.000)",[1737,1871],{"d":1872,"fill":1740,"stroke":1873,"style":1742},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","var(--text,#172033)",[833,1875,1679],{"x":1755,"y":1787,"style":1876},"font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[833,1878,1879],{"x":1755,"y":1791,"style":1613},"left merge",[42,1881,1883],{"id":1882},"common-pitfalls","Common pitfalls",[1885,1886,1887,1895,1901,1907,1916],"ul",{},[1888,1889,1890,1894],"li",{},[1891,1892,1893],"strong",{},"Trusting a first-match lookup."," It hides every match after the first; check how many matches exist.",[1888,1896,1897,1900],{},[1891,1898,1899],{},"Joined text used for analysis."," Keep a row-per-match table for anything you calculate.",[1888,1902,1903,1906],{},[1891,1904,1905],{},"Unsorted matches."," Sort before joining or numbering so \"first\" and \"latest\" mean something.",[1888,1908,1909,1912,1913,1915],{},[1891,1910,1911],{},"Customers with no matches dropped."," Use ",[14,1914,776],{}," or a left merge so they stay, with explicit blanks.",[1888,1917,1918,1925,1926,17,1928,1930],{},[1891,1919,1920,1921,1924],{},"Missing ",[14,1922,1923],{},"_xlfn"," prefixes."," ",[14,1927,33],{},[14,1929,29],{}," written from Python need them.",[42,1932,1934],{"id":1933},"conclusion","Conclusion",[10,1936,1937,1938,17,1941,1943,1944,1946],{},"A first-match lookup is wrong whenever a key can match more than once. In pandas, choose the shape that fits the job: join all matches into one cell with ",[14,1939,1940],{},"groupby().agg(\", \".join)",[14,1942,776],{}," for a readable report column, aggregate counts and totals per key for a summary, number matches with ",[14,1945,1227],{}," to pick the nth or spread them into columns, and keep one row per match with a left merge for analysis. Sort first so the order of matches is meaningful, and keep keys without matches visible as blanks.",[42,1948,1950],{"id":1949},"frequently-asked-questions","Frequently asked questions",[10,1952,1953,1956],{},[1891,1954,1955],{},"How do I return all matches instead of the first like VLOOKUP does?","\nGroup the matching table by the key, aggregate the values, for example with \", \".join, and map the result onto the lookup table. In Excel 365, TEXTJOIN with FILTER does the same.",[10,1958,1959,1962],{},[1891,1960,1961],{},"How do I get the second match for a key in pandas?","\nSort the data, number rows within each key with groupby().cumcount() + 1, and filter on number 2.",[10,1964,1965,1968],{},[1891,1966,1967],{},"Why does my merge return more rows than my lookup table?","\nThe key matches several rows in the other table, so the merge returns one row per match. That is correct for one-to-many data; use validate to catch it when keys should be unique.",[10,1970,1971,1974],{},[1891,1972,1973],{},"How do I count matches per key?","\nUse groupby on the key with size or count, then map or merge the result onto the lookup table, filling keys with no matches with 0.",[42,1976,1978],{"id":1977},"related","Related",[1885,1980,1981,1987,1994,2001],{},[1888,1982,1983,1984,1986],{},"Up one level: ",[36,1985,39],{"href":38}," — combining tables.",[1888,1988,1989,1993],{},[36,1990,1992],{"href":1991},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fxlookup-equivalent-in-pandas\u002F","XLOOKUP Equivalent in pandas"," — the single-match lookup.",[1888,1995,1996,2000],{},[36,1997,1999],{"href":1998},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Ftextjoin-equivalent-in-pandas\u002F","TEXTJOIN Equivalent in pandas"," — joining text more generally.",[1888,2002,2003,2007],{},[36,2004,2006],{"href":2005},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-two-excel-files-on-common-column-python\u002F","Merge Two Excel Files on a Common Column in Python"," — merges from the beginning.",[2009,2010,2011],"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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":52,"searchDepth":117,"depth":117,"links":2013},[2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024],{"id":44,"depth":117,"text":45},{"id":86,"depth":117,"text":87},{"id":553,"depth":117,"text":554},{"id":1014,"depth":117,"text":1015},{"id":1220,"depth":117,"text":1221},{"id":1435,"depth":117,"text":1436},{"id":1700,"depth":117,"text":1701},{"id":1882,"depth":117,"text":1883},{"id":1933,"depth":117,"text":1934},{"id":1949,"depth":117,"text":1950},{"id":1977,"depth":117,"text":1978},"2026-10-02","VLOOKUP and XLOOKUP stop at the first match, so a customer with five orders shows one. Return every match with pandas instead: all matching values joined into one cell like TEXTJOIN with FILTER, a count and a total alongside, the nth match, each match in its own column, and the one-to-many merge that keeps a row per match — with the spreadsheet's first-match result shown for comparison.","md",[2029,2031,2033,2035],{"q":1955,"a":2030},"Group the matching table by the key, aggregate the values, for example with \", \".join, and map the result onto the lookup table. In Excel 365, TEXTJOIN with FILTER does the same.",{"q":1961,"a":2032},"Sort the data, number rows within each key with groupby().cumcount() + 1, and filter on number 2.",{"q":1967,"a":2034},"The key matches several rows in the other table, so the merge returns one row per match. That is correct for one-to-many data; use validate to catch it when keys should be unique.",{"q":1973,"a":2036},"Use groupby on the key with size or count, then map or merge the result onto the lookup table, filling keys with no matches with 0.","look up all matches for a key instead of the first",null,{"breadcrumb":2040},[2041,2044,2047],{"name":2042,"item":2043},"Home","\u002F",{"name":2045,"item":2046},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":2048,"item":38},"Merging and Joining Excel DataFrames with Pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Flook-up-all-matches-for-a-key-instead-of-the-first",{"title":2051,"description":2052},"Look Up All Matches for a Key in pandas (Not Just the First)","Return every match instead of VLOOKUP's first: groupby join like TEXTJOIN+FILTER, counts and totals per key, the nth match with cumcount, matches as columns, and one-to-many merges, verified against spreadsheet formulas.","look-up-all-matches-for-a-key-instead-of-the-first","advanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Flook-up-all-matches-for-a-key-instead-of-the-first\u002Findex",[2056],"joins-and-lookups","how-to","Mt3PbUNm5Ly92PeM3PO8PFDtdHaBIp6B3HRDOCyPKg8",[2060,2064],{"title":2061,"path":2062,"stem":2063,"children":-1},"Join Rows to Effective Date Ranges with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fjoin-rows-to-effective-date-ranges-with-pandas","advanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fjoin-rows-to-effective-date-ranges-with-pandas\u002Findex",{"title":2065,"path":2066,"stem":2067,"children":-1},"Match Rows by Nearest Date with pandas merge_asof","\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmatch-rows-by-nearest-date-with-merge-asof","advanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmatch-rows-by-nearest-date-with-merge-asof\u002Findex",[2069,2071],{"path":2070,"title":2045},"\u002Fadvanced-data-transformation-and-cleaning",{"path":2072,"title":2048},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes",[],[2075,2078,2081,2084,2087,2090],{"path":2076,"title":2077,"kind":2057},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcompare-two-excel-files-for-differences-with-python","Compare Two Excel Files for Differences with Python",{"path":2079,"title":2080,"kind":2057},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fconvert-multi-currency-transactions-with-an-fx-rate-table","Convert Multi-Currency Transactions with an FX Rate Table",{"path":2082,"title":2083,"kind":2057},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-differences-between-two-columns-with-openpyxl","Highlight Differences Between Two Columns with openpyxl",{"path":2085,"title":2086,"kind":2057},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fmap-free-text-job-titles-to-standard-categories","Map Free-Text Job Titles to Standard Categories",{"path":2088,"title":2089,"kind":2057},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fstandardise-category-values-with-a-mapping-table","Standardise Category Values with a Mapping Table in pandas",{"path":2091,"title":2092,"kind":2057},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fapproximate-match-vlookup-in-pandas","Approximate-Match VLOOKUP for Tiered Rates in pandas",1790972096305]