[{"data":1,"prerenderedAt":2137},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-more-than-two-excel-files-on-a-key-column":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-more-than-two-excel-files-on-a-key-column":2103,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-more-than-two-excel-files-on-a-key-column":2112,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-more-than-two-excel-files-on-a-key-column":2117,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-more-than-two-excel-files-on-a-key-column":2118},{"id":4,"title":5,"body":6,"dateModified":2068,"datePublished":2068,"description":2069,"extension":2070,"faq":2071,"keyword":2080,"kind":2081,"meta":2082,"navigation":285,"path":2092,"seo":2093,"slug":2096,"stem":2097,"topics":2098,"type":2101,"__hash__":2102},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-more-than-two-excel-files-on-a-key-column\u002Findex.md","Merge More Than Two Excel Files on a Key Column",{"type":7,"value":8,"toc":2054},"minimark",[9,24,29,57,72,236,240,243,528,543,547,550,743,760,764,782,935,960,1127,1131,1134,1311,1314,1545,1549,1560,1624,1634,1638,1645,1885,1891,1895,1902,1906,1975,1979,1989,1993,1999,2005,2011,2017,2021,2050],[10,11,12,13,17,18,23],"p",{},"Joining two workbooks on a shared ID is one ",[14,15,16],"code",{},"merge"," call. Real reporting jobs usually have more sources than that: an HR export with names and departments, a payroll file with salaries, an IT asset list with laptops, a training system export with course completions — four files, one employee ID, and a request for \"one sheet with everything\". Chaining merges by hand works until a file is added, a column name clashes, or one file turns out to list an employee twice and quietly doubles their salary in the result. This page, part of ",[19,20,22],"a",{"href":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002F","Merging and Joining Excel DataFrames",", builds the merge as a loop over any number of files, with the checks that keep it honest.",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,37,38],{"__ignoreMap":35},[39,40,43,47,51,54],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sMTad","pip",[39,48,50],{"class":49},"srMev"," install",[39,52,53],{"class":49}," pandas",[39,55,56],{"class":49}," openpyxl\n",[10,58,59,60,63,64,67,68,71],{},"The ",[14,61,62],{},"sources\u002F"," folder holds four exports. They share an employee ID but disagree in the ways real exports do: payroll calls the column ",[14,65,66],{},"Employee ID",", training writes it in lower case with lower-case values, HR and payroll both have a ",[14,69,70],{},"Status"," column that means different things, payroll includes an employee (E07) HR does not know about, and the IT list gives E06 two laptops.",[73,74,82,83,82,87,82,91,82,99,82,109,82,120,82,127,82,132,82,140,82,147,82,153,82,158,82,161,82,166,82,169,82,175,82,179,82,183,82,186,82,188,82,193,82,200,82,205,82,209,82,212,82,214,82,217,82,223,82,227,82,231,82,234],"svg",{"viewBox":75,"role":76,"ariaLabelledBy":77,"xmlns":80,"style":81},"4 0 752 436","img",[78,79],"mg-fan-t","mg-fan-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[84,85,86],"title",{"id":78},"Four sources, one key, one wide table",[88,89,90],"desc",{"id":79},"Four source files — HR, payroll, IT assets and training — fan in to a single merged table keyed by employee ID.",[92,93],"rect",{"x":94,"y":95,"width":96,"height":97,"fill":98},"4","0","752","436","#ffffff",[92,100],{"x":101,"y":102,"width":103,"height":104,"rx":105,"fill":106,"stroke":107,"style":108},"520.0","171.5","220.0","93.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[110,111,113],"g",{"transform":112},"translate(618.0,184.5) scale(1.000)",[114,115],"path",{"d":116,"fill":117,"stroke":118,"style":119},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[121,122,126],"text",{"x":123,"y":124,"style":125},"630.0","231.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Merged table",[121,128,131],{"x":123,"y":129,"style":130},"250.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","one row per EmployeeID",[92,133],{"x":134,"y":135,"width":136,"height":137,"rx":105,"fill":138,"stroke":139,"style":108},"20.0","16.0","250.0","92.0","#d9f4f1","var(--teal,#0f9488)",[110,141,143],{"transform":142},"translate(133.0,29.0) scale(1.000)",[114,144],{"d":145,"fill":117,"stroke":146,"style":119},"M9 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z M2.5 20c0-3.6 2.9-6 6.5-6s6.5 2.4 6.5 6 M16 4.5a3.3 3.3 0 0 1 0 6.3 M18 14.3c2.2.7 3.5 2.8 3.5 5.7","var(--teal-ink,#0b6157)",[121,148,152],{"x":149,"y":150,"style":151},"145.0","75.0","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","hr.xlsx",[121,154,157],{"x":149,"y":155,"style":156},"94.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","name, department, status",[114,159],{"d":160,"fill":117,"stroke":107,"style":108},"M274.0,62.0 C394.0,62.0 394.0,218.0 510.0,218.0",[162,163],"polygon",{"points":164,"fill":165},"514.0,218.0 505.0,223.0 505.0,213.0","#5b5cf0",[92,167],{"x":134,"y":168,"width":136,"height":137,"rx":105,"fill":138,"stroke":139,"style":108},"120.0",[110,170,172],{"transform":171},"translate(133.0,133.0) scale(1.000)",[114,173],{"d":174,"fill":117,"stroke":146,"style":119},"M5 6c0-1.7 3.1-3 7-3s7 1.3 7 3-3.1 3-7 3-7-1.3-7-3z M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6 M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3",[121,176,178],{"x":149,"y":177,"style":151},"179.0","payroll.xlsx",[121,180,182],{"x":149,"y":181,"style":156},"198.0","salary, pay status",[114,184],{"d":185,"fill":117,"stroke":107,"style":108},"M274.0,166.0 C394.0,166.0 394.0,218.0 510.0,218.0",[162,187],{"points":164,"fill":165},[92,189],{"x":134,"y":190,"width":136,"height":137,"rx":105,"fill":191,"stroke":192,"style":108},"224.0","#fdefd8","var(--gold,#b4740a)",[110,194,196],{"transform":195},"translate(133.0,237.0) scale(1.000)",[114,197],{"d":198,"fill":117,"stroke":199,"style":119},"M4 4h16v6H4z M4 14h16v6H4z M8 7h.01 M8 17h.01","var(--gold-ink,#7a4e06)",[121,201,204],{"x":149,"y":202,"style":203},"283.0","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","it_assets.xlsx",[121,206,208],{"x":149,"y":207,"style":156},"302.0","laptop",[114,210],{"d":211,"fill":117,"stroke":107,"style":108},"M274.0,270.0 C394.0,270.0 394.0,218.0 510.0,218.0",[162,213],{"points":164,"fill":165},[92,215],{"x":134,"y":216,"width":136,"height":137,"rx":105,"fill":191,"stroke":192,"style":108},"328.0",[110,218,220],{"transform":219},"translate(133.0,341.0) scale(1.000)",[114,221],{"d":222,"fill":117,"stroke":199,"style":119},"M5 12.5l4.5 4.5L19 7",[121,224,226],{"x":149,"y":225,"style":203},"387.0","training.xlsx",[121,228,230],{"x":149,"y":229,"style":156},"406.0","courses completed",[114,232],{"d":233,"fill":117,"stroke":107,"style":108},"M274.0,374.0 C394.0,374.0 394.0,218.0 510.0,218.0",[162,235],{"points":164,"fill":165},[25,237,239],{"id":238},"step-1-load-every-file-with-a-consistent-key","Step 1 — Load every file with a consistent key",[10,241,242],{},"Before any merge, make the key identical everywhere: same column name, same type, same case and no stray spaces. Do it once per file, in a loader, rather than patching merges later:",[30,244,248],{"className":245,"code":246,"language":247,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","from pathlib import Path\nimport pandas as pd\n\ndef load(path, key=\"EmployeeID\"):\n    df = pd.read_excel(path, dtype=str)\n    rename = {c: key for c in df.columns if c.replace(\" \", \"\").lower() == key.lower()}\n    df = df.rename(columns=rename)\n    df[key] = df[key].str.strip().str.upper()\n    return df\n\nsources = {p.stem: load(p) for p in sorted(Path(\"sources\").glob(\"*.xlsx\"))}\nfor name, df in sources.items():\n    print(f\"{name:10} {len(df)} rows  {list(df.columns)}\")\n","python",[14,249,250,266,280,287,309,333,380,398,409,418,423,459,472],{"__ignoreMap":35},[39,251,252,256,260,263],{"class":41,"line":42},[39,253,255],{"class":254},"s-kum","from",[39,257,259],{"class":258},"skGVy"," pathlib ",[39,261,262],{"class":254},"import",[39,264,265],{"class":258}," Path\n",[39,267,269,271,274,277],{"class":41,"line":268},2,[39,270,262],{"class":254},[39,272,273],{"class":258}," pandas ",[39,275,276],{"class":254},"as",[39,278,279],{"class":258}," pd\n",[39,281,283],{"class":41,"line":282},3,[39,284,286],{"emptyLinePlaceholder":285},true,"\n",[39,288,290,293,297,300,303,306],{"class":41,"line":289},4,[39,291,292],{"class":254},"def",[39,294,296],{"class":295},"s_Opv"," load",[39,298,299],{"class":258},"(path, key",[39,301,302],{"class":254},"=",[39,304,305],{"class":49},"\"EmployeeID\"",[39,307,308],{"class":258},"):\n",[39,310,312,315,317,320,324,326,330],{"class":41,"line":311},5,[39,313,314],{"class":258},"    df ",[39,316,302],{"class":254},[39,318,319],{"class":258}," pd.read_excel(path, ",[39,321,323],{"class":322},"sa561","dtype",[39,325,302],{"class":254},[39,327,329],{"class":328},"sP0c6","str",[39,331,332],{"class":258},")\n",[39,334,336,339,341,344,347,350,353,356,359,362,365,368,371,374,377],{"class":41,"line":335},6,[39,337,338],{"class":258},"    rename ",[39,340,302],{"class":254},[39,342,343],{"class":258}," {c: key ",[39,345,346],{"class":254},"for",[39,348,349],{"class":258}," c ",[39,351,352],{"class":254},"in",[39,354,355],{"class":258}," df.columns ",[39,357,358],{"class":254},"if",[39,360,361],{"class":258}," c.replace(",[39,363,364],{"class":49},"\" \"",[39,366,367],{"class":258},", ",[39,369,370],{"class":49},"\"\"",[39,372,373],{"class":258},").lower() ",[39,375,376],{"class":254},"==",[39,378,379],{"class":258}," key.lower()}\n",[39,381,383,385,387,390,393,395],{"class":41,"line":382},7,[39,384,314],{"class":258},[39,386,302],{"class":254},[39,388,389],{"class":258}," df.rename(",[39,391,392],{"class":322},"columns",[39,394,302],{"class":254},[39,396,397],{"class":258},"rename)\n",[39,399,401,404,406],{"class":41,"line":400},8,[39,402,403],{"class":258},"    df[key] ",[39,405,302],{"class":254},[39,407,408],{"class":258}," df[key].str.strip().str.upper()\n",[39,410,412,415],{"class":41,"line":411},9,[39,413,414],{"class":254},"    return",[39,416,417],{"class":258}," df\n",[39,419,421],{"class":41,"line":420},10,[39,422,286],{"emptyLinePlaceholder":285},[39,424,426,429,431,434,436,439,441,444,447,450,453,456],{"class":41,"line":425},11,[39,427,428],{"class":258},"sources ",[39,430,302],{"class":254},[39,432,433],{"class":258}," {p.stem: load(p) ",[39,435,346],{"class":254},[39,437,438],{"class":258}," p ",[39,440,352],{"class":254},[39,442,443],{"class":328}," sorted",[39,445,446],{"class":258},"(Path(",[39,448,449],{"class":49},"\"sources\"",[39,451,452],{"class":258},").glob(",[39,454,455],{"class":49},"\"*.xlsx\"",[39,457,458],{"class":258},"))}\n",[39,460,462,464,467,469],{"class":41,"line":461},12,[39,463,346],{"class":254},[39,465,466],{"class":258}," name, df ",[39,468,352],{"class":254},[39,470,471],{"class":258}," sources.items():\n",[39,473,475,478,481,484,487,491,494,497,500,503,506,509,511,514,516,519,522,524,526],{"class":41,"line":474},13,[39,476,477],{"class":328},"    print",[39,479,480],{"class":258},"(",[39,482,483],{"class":254},"f",[39,485,486],{"class":49},"\"",[39,488,490],{"class":489},"sSjpA","{",[39,492,493],{"class":258},"name",[39,495,496],{"class":254},":10",[39,498,499],{"class":489},"}",[39,501,502],{"class":489}," {",[39,504,505],{"class":328},"len",[39,507,508],{"class":258},"(df)",[39,510,499],{"class":489},[39,512,513],{"class":49}," rows  ",[39,515,490],{"class":489},[39,517,518],{"class":328},"list",[39,520,521],{"class":258},"(df.columns)",[39,523,499],{"class":489},[39,525,486],{"class":49},[39,527,332],{"class":258},[10,529,530,531,534,535,538,539,542],{},"Reading everything as text with ",[14,532,533],{},"dtype=str"," avoids the other classic key mismatch, where one file stores IDs as numbers (",[14,536,537],{},"101",") and another as text (",[14,540,541],{},"\"0101\"","). Convert non-key columns to numbers after the merge, where you know which ones should be numeric.",[25,544,546],{"id":545},"step-2-check-each-files-key-before-merging","Step 2 — Check each file's key before merging",[10,548,549],{},"A merge on a key that is not unique multiplies rows: two laptop rows for E06 turn E06 into two rows in the result, and any sum over salary counts E06 twice. Check uniqueness per file first and decide what to do with each duplicate:",[30,551,553],{"className":245,"code":552,"language":247,"meta":35,"style":35},"for name, df in sources.items():\n    dupes = df[df.duplicated(\"EmployeeID\", keep=False)]\n    if len(dupes):\n        print(f\"{name}: duplicate keys\\n{dupes.to_string(index=False)}\")\n\nsources[\"it_assets\"] = (sources[\"it_assets\"].groupby(\"EmployeeID\", as_index=False)\n                        .agg(Laptop=(\"Laptop\", \", \".join), Laptops=(\"Laptop\", \"size\")))\nprint(sources[\"it_assets\"].to_string(index=False))\n",[14,554,555,565,590,601,643,647,681,721],{"__ignoreMap":35},[39,556,557,559,561,563],{"class":41,"line":42},[39,558,346],{"class":254},[39,560,466],{"class":258},[39,562,352],{"class":254},[39,564,471],{"class":258},[39,566,567,570,572,575,577,579,582,584,587],{"class":41,"line":268},[39,568,569],{"class":258},"    dupes ",[39,571,302],{"class":254},[39,573,574],{"class":258}," df[df.duplicated(",[39,576,305],{"class":49},[39,578,367],{"class":258},[39,580,581],{"class":322},"keep",[39,583,302],{"class":254},[39,585,586],{"class":328},"False",[39,588,589],{"class":258},")]\n",[39,591,592,595,598],{"class":41,"line":282},[39,593,594],{"class":254},"    if",[39,596,597],{"class":328}," len",[39,599,600],{"class":258},"(dupes):\n",[39,602,603,606,608,610,612,614,616,618,621,624,627,630,632,634,637,639,641],{"class":41,"line":289},[39,604,605],{"class":328},"        print",[39,607,480],{"class":258},[39,609,483],{"class":254},[39,611,486],{"class":49},[39,613,490],{"class":489},[39,615,493],{"class":258},[39,617,499],{"class":489},[39,619,620],{"class":49},": duplicate keys",[39,622,623],{"class":489},"\\n{",[39,625,626],{"class":258},"dupes.to_string(",[39,628,629],{"class":322},"index",[39,631,302],{"class":254},[39,633,586],{"class":328},[39,635,636],{"class":258},")",[39,638,499],{"class":489},[39,640,486],{"class":49},[39,642,332],{"class":258},[39,644,645],{"class":41,"line":311},[39,646,286],{"emptyLinePlaceholder":285},[39,648,649,652,655,658,660,663,665,668,670,672,675,677,679],{"class":41,"line":335},[39,650,651],{"class":258},"sources[",[39,653,654],{"class":49},"\"it_assets\"",[39,656,657],{"class":258},"] ",[39,659,302],{"class":254},[39,661,662],{"class":258}," (sources[",[39,664,654],{"class":49},[39,666,667],{"class":258},"].groupby(",[39,669,305],{"class":49},[39,671,367],{"class":258},[39,673,674],{"class":322},"as_index",[39,676,302],{"class":254},[39,678,586],{"class":328},[39,680,332],{"class":258},[39,682,683,686,689,691,693,696,698,701,704,707,709,711,713,715,718],{"class":41,"line":382},[39,684,685],{"class":258},"                        .agg(",[39,687,688],{"class":322},"Laptop",[39,690,302],{"class":254},[39,692,480],{"class":258},[39,694,695],{"class":49},"\"Laptop\"",[39,697,367],{"class":258},[39,699,700],{"class":49},"\", \"",[39,702,703],{"class":258},".join), ",[39,705,706],{"class":322},"Laptops",[39,708,302],{"class":254},[39,710,480],{"class":258},[39,712,695],{"class":49},[39,714,367],{"class":258},[39,716,717],{"class":49},"\"size\"",[39,719,720],{"class":258},")))\n",[39,722,723,726,729,731,734,736,738,740],{"class":41,"line":400},[39,724,725],{"class":328},"print",[39,727,728],{"class":258},"(sources[",[39,730,654],{"class":49},[39,732,733],{"class":258},"].to_string(",[39,735,629],{"class":322},[39,737,302],{"class":254},[39,739,586],{"class":328},[39,741,742],{"class":258},"))\n",[10,744,745,746,750,751,755,756,759],{},"Here the duplicates are legitimate — someone can have two laptops — so aggregate them to one row per employee: the asset tags joined into one cell and a count. In other files duplicates are errors to report instead. Either way, the decision is made ",[747,748,749],"em",{},"before"," the merge, where it is visible, instead of discovered afterwards in an inflated total. ",[19,752,754],{"href":753},"\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"," covers the ",[14,757,758],{},"validate="," option used in the next step.",[25,761,763],{"id":762},"step-3-prefix-the-columns-then-merge-in-a-loop","Step 3 — Prefix the columns, then merge in a loop",[10,765,766,767,769,770,773,774,777,778,781],{},"Two files with a ",[14,768,70],{}," column would come out as ",[14,771,772],{},"Status_x"," and ",[14,775,776],{},"Status_y",", which says nothing about where each came from. Prefix every non-key column with its source, then merge the files one after another with ",[14,779,780],{},"functools.reduce",":",[30,783,785],{"className":245,"code":784,"language":247,"meta":35,"style":35},"from functools import reduce\n\nprefixed = [df.set_index(\"EmployeeID\").add_prefix(f\"{name}.\").reset_index()\n            for name, df in sources.items()]\n\ndef join(left, right):\n    return left.merge(right, on=\"EmployeeID\", how=\"outer\", validate=\"one_to_one\")\n\nwide = reduce(join, prefixed)\nprint(wide.shape)\nprint(wide.to_string(index=False))\n",[14,786,787,799,803,834,846,850,860,896,900,913,920],{"__ignoreMap":35},[39,788,789,791,794,796],{"class":41,"line":42},[39,790,255],{"class":254},[39,792,793],{"class":258}," functools ",[39,795,262],{"class":254},[39,797,798],{"class":322}," reduce\n",[39,800,801],{"class":41,"line":268},[39,802,286],{"emptyLinePlaceholder":285},[39,804,805,808,810,813,815,818,820,822,824,826,828,831],{"class":41,"line":282},[39,806,807],{"class":258},"prefixed ",[39,809,302],{"class":254},[39,811,812],{"class":258}," [df.set_index(",[39,814,305],{"class":49},[39,816,817],{"class":258},").add_prefix(",[39,819,483],{"class":254},[39,821,486],{"class":49},[39,823,490],{"class":489},[39,825,493],{"class":258},[39,827,499],{"class":489},[39,829,830],{"class":49},".\"",[39,832,833],{"class":258},").reset_index()\n",[39,835,836,839,841,843],{"class":41,"line":289},[39,837,838],{"class":254},"            for",[39,840,466],{"class":258},[39,842,352],{"class":254},[39,844,845],{"class":258}," sources.items()]\n",[39,847,848],{"class":41,"line":311},[39,849,286],{"emptyLinePlaceholder":285},[39,851,852,854,857],{"class":41,"line":335},[39,853,292],{"class":254},[39,855,856],{"class":295}," join",[39,858,859],{"class":258},"(left, right):\n",[39,861,862,864,867,870,872,874,876,879,881,884,886,889,891,894],{"class":41,"line":382},[39,863,414],{"class":254},[39,865,866],{"class":258}," left.merge(right, ",[39,868,869],{"class":322},"on",[39,871,302],{"class":254},[39,873,305],{"class":49},[39,875,367],{"class":258},[39,877,878],{"class":322},"how",[39,880,302],{"class":254},[39,882,883],{"class":49},"\"outer\"",[39,885,367],{"class":258},[39,887,888],{"class":322},"validate",[39,890,302],{"class":254},[39,892,893],{"class":49},"\"one_to_one\"",[39,895,332],{"class":258},[39,897,898],{"class":41,"line":400},[39,899,286],{"emptyLinePlaceholder":285},[39,901,902,905,907,910],{"class":41,"line":411},[39,903,904],{"class":258},"wide ",[39,906,302],{"class":254},[39,908,909],{"class":322}," reduce",[39,911,912],{"class":258},"(join, prefixed)\n",[39,914,915,917],{"class":41,"line":420},[39,916,725],{"class":328},[39,918,919],{"class":258},"(wide.shape)\n",[39,921,922,924,927,929,931,933],{"class":41,"line":425},[39,923,725],{"class":328},[39,925,926],{"class":258},"(wide.to_string(",[39,928,629],{"class":322},[39,930,302],{"class":254},[39,932,586],{"class":328},[39,934,742],{"class":258},[10,936,937,940,941,944,945,948,949,952,953,367,956,959],{},[14,938,939],{},"reduce(join, [a, b, c, d])"," computes ",[14,942,943],{},"join(join(join(a, b), c), d)"," — the chained merge, written once for any number of files. ",[14,946,947],{},"how=\"outer\""," keeps every employee who appears in any file; an inner join would silently drop E07, who is only in payroll, and E04, who has no laptop. ",[14,950,951],{},"validate=\"one_to_one\""," makes pandas raise if either side of any merge has a duplicate key, so a new duplicate in next month's export stops the job instead of corrupting it. The prefixed names — ",[14,954,955],{},"hr.Status",[14,957,958],{},"payroll.Status"," — say exactly where each value came from.",[73,961,82,966,82,969,82,972,82,975,82,981,82,987,82,993,82,997,82,1001,82,1006,82,1009,82,1012,82,1018,82,1023,82,1026,82,1030,82,1034,82,1038,82,1041,82,1049,82,1054,82,1057,82,1062,82,1066,82,1068,82,1070,82,1074,82,1077,82,1080,82,1083,82,1086,82,1091,82,1095,82,1097,82,1099,82,1103,82,1106,82,1109,82,1114,82,1119,82,1122,82,1125],{"viewBox":962,"role":76,"ariaLabelledBy":963,"xmlns":80,"style":81},"4 0 752 160",[964,965],"mg-reduce-t","mg-reduce-d",[84,967,968],{"id":964},"How reduce chains the merges",[88,970,971],{"id":965},"The HR table is merged with IT assets, the result with payroll, and that result with training — the files in alphabetical order; each step is an outer merge validated as one-to-one.",[92,973],{"x":94,"y":95,"width":96,"height":974,"fill":98},"160",[92,976],{"x":134,"y":135,"width":977,"height":978,"rx":105,"fill":979,"stroke":980,"style":108},"90.7","128.0","#e7ebef","var(--line,#cdd5e6)",[110,982,984],{"transform":983},"translate(53.3,37.8) scale(1.000)",[114,985],{"d":145,"fill":117,"stroke":986,"style":119},"var(--text,#172033)",[121,988,992],{"x":989,"y":990,"style":991},"65.3","84.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","hr",[121,994,996],{"x":989,"y":995,"style":130},"103.8","6",[121,998,1000],{"x":989,"y":999,"style":130},"121.2","employees",[41,1002],{"x1":1003,"y1":1004,"x2":1005,"y2":1004,"stroke":107,"style":108},"115.7","80.0","168.3",[162,1007],{"points":1008,"fill":165},"172.3,80.0 163.3,85.0 163.3,75.0",[92,1010],{"x":1011,"y":135,"width":977,"height":978,"rx":105,"fill":106,"stroke":107,"style":108},"177.3",[110,1013,1015],{"transform":1014},"translate(210.7,29.0) scale(1.000)",[114,1016],{"d":1017,"fill":117,"stroke":118,"style":119},"M6 4v5a5 5 0 0 0 5 5h7 M6 20v-5 M15 11l3 3-3 3",[121,1019,1022],{"x":1020,"y":1021,"style":125},"222.7","75.5","+",[121,1024,1025],{"x":1020,"y":104,"style":125},"it_assets",[121,1027,1029],{"x":1020,"y":1028,"style":130},"112.5","outer,",[121,1031,1033],{"x":1020,"y":1032,"style":130},"130.0","one_to_one",[41,1035],{"x1":1036,"y1":1004,"x2":1037,"y2":1004,"stroke":107,"style":108},"273.0","325.7",[162,1039],{"points":1040,"fill":165},"329.7,80.0 320.7,85.0 320.7,75.0",[92,1042],{"x":1043,"y":1044,"width":1045,"height":1046,"rx":1047,"fill":98,"stroke":980,"style":1048},"276.0","49.0","50.7","19.0","9","stroke-width:1px",[121,1050,16],{"x":1051,"y":1052,"style":1053},"301.3","62.5","font-size:10.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle",[92,1055],{"x":1056,"y":135,"width":977,"height":978,"rx":105,"fill":106,"stroke":107,"style":108},"334.7",[110,1058,1060],{"transform":1059},"translate(368.0,37.8) scale(1.000)",[114,1061],{"d":1017,"fill":117,"stroke":118,"style":119},[121,1063,1065],{"x":1064,"y":990,"style":125},"380.0","+ payroll",[121,1067,1029],{"x":1064,"y":995,"style":130},[121,1069,1033],{"x":1064,"y":999,"style":130},[41,1071],{"x1":1072,"y1":1004,"x2":1073,"y2":1004,"stroke":107,"style":108},"430.3","483.0",[162,1075],{"points":1076,"fill":165},"487.0,80.0 478.0,85.0 478.0,75.0",[92,1078],{"x":1079,"y":1044,"width":1045,"height":1046,"rx":1047,"fill":98,"stroke":980,"style":1048},"433.3",[121,1081,16],{"x":1082,"y":1052,"style":1053},"458.7",[92,1084],{"x":1085,"y":135,"width":977,"height":978,"rx":105,"fill":106,"stroke":107,"style":108},"492.0",[110,1087,1089],{"transform":1088},"translate(525.3,37.8) scale(1.000)",[114,1090],{"d":1017,"fill":117,"stroke":118,"style":119},[121,1092,1094],{"x":1093,"y":990,"style":125},"537.3","+ training",[121,1096,1029],{"x":1093,"y":995,"style":130},[121,1098,1033],{"x":1093,"y":999,"style":130},[41,1100],{"x1":1101,"y1":1004,"x2":1102,"y2":1004,"stroke":107,"style":108},"587.7","640.3",[162,1104],{"points":1105,"fill":165},"644.3,80.0 635.3,85.0 635.3,75.0",[92,1107],{"x":1108,"y":135,"width":977,"height":978,"rx":105,"fill":138,"stroke":139,"style":108},"649.3",[110,1110,1112],{"transform":1111},"translate(682.7,29.0) scale(1.000)",[114,1113],{"d":116,"fill":117,"stroke":146,"style":119},[121,1115,1118],{"x":1116,"y":1021,"style":1117},"694.7","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","wide",[121,1120,1121],{"x":1116,"y":104,"style":1117},"table",[121,1123,1124],{"x":1116,"y":1028,"style":130},"7",[121,1126,1000],{"x":1116,"y":1032,"style":130},[25,1128,1130],{"id":1129},"step-4-record-where-each-key-was-found","Step 4 — Record where each key was found",[10,1132,1133],{},"After an outer merge, a missing value can mean \"this file has the employee but the field is blank\" or \"this file does not have the employee at all\". Add a presence flag per source before merging, so the difference survives:",[30,1135,1137],{"className":245,"code":1136,"language":247,"meta":35,"style":35},"flagged = [df.assign(**{f\"in_{name}\": True}) for name, df in zip(sources, prefixed)]\nwide = reduce(join, flagged)\nflags = [f\"in_{n}\" for n in sources]\nwide[flags] = wide[flags].fillna(False).astype(bool)\nwide[\"Sources\"] = wide[flags].sum(axis=1)\nprint(wide[[\"EmployeeID\"] + flags + [\"Sources\"]].to_string(index=False))\n",[14,1138,1139,1188,1199,1233,1253,1278],{"__ignoreMap":35},[39,1140,1141,1144,1146,1149,1152,1154,1156,1159,1161,1163,1165,1167,1170,1173,1176,1178,1180,1182,1185],{"class":41,"line":42},[39,1142,1143],{"class":258},"flagged ",[39,1145,302],{"class":254},[39,1147,1148],{"class":258}," [df.assign(",[39,1150,1151],{"class":254},"**",[39,1153,490],{"class":258},[39,1155,483],{"class":254},[39,1157,1158],{"class":49},"\"in_",[39,1160,490],{"class":489},[39,1162,493],{"class":258},[39,1164,499],{"class":489},[39,1166,486],{"class":49},[39,1168,1169],{"class":258},": ",[39,1171,1172],{"class":328},"True",[39,1174,1175],{"class":258},"}) ",[39,1177,346],{"class":254},[39,1179,466],{"class":258},[39,1181,352],{"class":254},[39,1183,1184],{"class":328}," zip",[39,1186,1187],{"class":258},"(sources, prefixed)]\n",[39,1189,1190,1192,1194,1196],{"class":41,"line":268},[39,1191,904],{"class":258},[39,1193,302],{"class":254},[39,1195,909],{"class":322},[39,1197,1198],{"class":258},"(join, flagged)\n",[39,1200,1201,1204,1206,1209,1211,1213,1215,1218,1220,1222,1225,1228,1230],{"class":41,"line":282},[39,1202,1203],{"class":258},"flags ",[39,1205,302],{"class":254},[39,1207,1208],{"class":258}," [",[39,1210,483],{"class":254},[39,1212,1158],{"class":49},[39,1214,490],{"class":489},[39,1216,1217],{"class":258},"n",[39,1219,499],{"class":489},[39,1221,486],{"class":49},[39,1223,1224],{"class":254}," for",[39,1226,1227],{"class":258}," n ",[39,1229,352],{"class":254},[39,1231,1232],{"class":258}," sources]\n",[39,1234,1235,1238,1240,1243,1245,1248,1251],{"class":41,"line":289},[39,1236,1237],{"class":258},"wide[flags] ",[39,1239,302],{"class":254},[39,1241,1242],{"class":258}," wide[flags].fillna(",[39,1244,586],{"class":328},[39,1246,1247],{"class":258},").astype(",[39,1249,1250],{"class":328},"bool",[39,1252,332],{"class":258},[39,1254,1255,1258,1261,1263,1265,1268,1271,1273,1276],{"class":41,"line":311},[39,1256,1257],{"class":258},"wide[",[39,1259,1260],{"class":49},"\"Sources\"",[39,1262,657],{"class":258},[39,1264,302],{"class":254},[39,1266,1267],{"class":258}," wide[flags].sum(",[39,1269,1270],{"class":322},"axis",[39,1272,302],{"class":254},[39,1274,1275],{"class":328},"1",[39,1277,332],{"class":258},[39,1279,1280,1282,1285,1287,1289,1291,1294,1296,1298,1300,1303,1305,1307,1309],{"class":41,"line":335},[39,1281,725],{"class":328},[39,1283,1284],{"class":258},"(wide[[",[39,1286,305],{"class":49},[39,1288,657],{"class":258},[39,1290,1022],{"class":254},[39,1292,1293],{"class":258}," flags ",[39,1295,1022],{"class":254},[39,1297,1208],{"class":258},[39,1299,1260],{"class":49},[39,1301,1302],{"class":258},"]].to_string(",[39,1304,629],{"class":322},[39,1306,302],{"class":254},[39,1308,586],{"class":328},[39,1310,742],{"class":258},[10,1312,1313],{},"The flags make the coverage problems obvious. E07 appears only in payroll — someone is being paid who is not in the HR system. E06 is the mirror image: an active employee with two laptops and a training record, but no payroll entry at all. Both are the kind of finding an audit is looking for, and neither would be visible in an inner join. E04, a leaver, has no laptop and no training record, which is expected. E02 has not completed any training. Each of these is a question for a different team, and the flags let the report route them.",[73,1315,82,1320,82,1323,82,1326,82,1329,82,1333,82,1338,82,1343,82,1346,82,1350,82,1353,82,1356,82,1359,82,1363,82,1367,82,1373,82,1375,82,1381,82,1383,82,1388,82,1390,82,1395,82,1397,82,1402,82,1405,82,1409,82,1411,82,1416,82,1418,82,1423,82,1425,82,1430,82,1432,82,1440,82,1443,82,1447,82,1449,82,1454,82,1456,82,1461,82,1463,82,1468,82,1470,82,1475,82,1478,82,1482,82,1484,82,1489,82,1491,82,1496,82,1498,82,1503,82,1505,82,1510,82,1513,82,1517,82,1519,82,1524,82,1526,82,1531,82,1533,82,1538,82,1540],{"viewBox":1316,"role":76,"ariaLabelledBy":1317,"xmlns":80,"style":81},"4 0 752 242",[1318,1319],"mg-coverage-t","mg-coverage-d",[84,1321,1322],{"id":1318},"Which sources know each employee",[88,1324,1325],{"id":1319},"A grid of employees against the four sources. Most employees appear in HR and payroll. E04 has no laptop or training; E02 no training; E06 is missing from payroll; E07 appears only in payroll.",[92,1327],{"x":94,"y":95,"width":96,"height":1328,"fill":98},"242",[92,1330],{"x":134,"y":135,"width":1331,"height":1332,"rx":95,"fill":98,"stroke":980,"style":1048},"53.7","40.0",[92,1334],{"x":1335,"y":135,"width":1336,"height":1332,"rx":95,"fill":106,"stroke":107,"style":1337},"73.7","166.6","stroke-width:1.2px",[121,1339,992],{"x":1340,"y":1341,"style":1342},"157.0","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[92,1344],{"x":1345,"y":135,"width":1336,"height":1332,"rx":95,"fill":106,"stroke":107,"style":1337},"240.3",[121,1347,1349],{"x":1348,"y":1341,"style":1342},"323.6","payroll",[92,1351],{"x":1352,"y":135,"width":1336,"height":1332,"rx":95,"fill":106,"stroke":107,"style":1337},"406.8",[121,1354,1025],{"x":1355,"y":1341,"style":1342},"490.1",[92,1357],{"x":1358,"y":135,"width":1336,"height":1332,"rx":95,"fill":106,"stroke":107,"style":1337},"573.4",[121,1360,1362],{"x":1361,"y":1341,"style":1342},"656.7","training",[92,1364],{"x":134,"y":1365,"width":1331,"height":1366,"rx":95,"fill":979,"stroke":980,"style":1048},"56.0","34.0",[121,1368,1372],{"x":1369,"y":1370,"style":1371},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","E01",[92,1374],{"x":1335,"y":1365,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1376,1378],{"transform":1377},"translate(148.0,64.0) scale(0.750)",[114,1379],{"d":222,"fill":117,"stroke":139,"style":1380},"stroke-width:2.40px;stroke-linecap:round;stroke-linejoin:round",[92,1382],{"x":1345,"y":1365,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1384,1386],{"transform":1385},"translate(314.6,64.0) scale(0.750)",[114,1387],{"d":222,"fill":117,"stroke":139,"style":1380},[92,1389],{"x":1352,"y":1365,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1391,1393],{"transform":1392},"translate(481.1,64.0) scale(0.750)",[114,1394],{"d":222,"fill":117,"stroke":139,"style":1380},[92,1396],{"x":1358,"y":1365,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1398,1400],{"transform":1399},"translate(647.7,64.0) scale(0.750)",[114,1401],{"d":222,"fill":117,"stroke":139,"style":1380},[92,1403],{"x":134,"y":1404,"width":1331,"height":1366,"rx":95,"fill":979,"stroke":980,"style":1048},"90.0",[121,1406,1408],{"x":1369,"y":1407,"style":1371},"112.0","E02",[92,1410],{"x":1335,"y":1404,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1412,1414],{"transform":1413},"translate(148.0,98.0) scale(0.750)",[114,1415],{"d":222,"fill":117,"stroke":139,"style":1380},[92,1417],{"x":1345,"y":1404,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1419,1421],{"transform":1420},"translate(314.6,98.0) scale(0.750)",[114,1422],{"d":222,"fill":117,"stroke":139,"style":1380},[92,1424],{"x":1352,"y":1404,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1426,1428],{"transform":1427},"translate(481.1,98.0) scale(0.750)",[114,1429],{"d":222,"fill":117,"stroke":139,"style":1380},[92,1431],{"x":1358,"y":1404,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1433,1435],{"transform":1434},"translate(648.7,99.0) scale(0.667)",[114,1436],{"d":1437,"fill":117,"stroke":1438,"style":1439},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)","stroke-width:2.70px;stroke-linecap:round;stroke-linejoin:round",[92,1441],{"x":134,"y":1442,"width":1331,"height":1366,"rx":95,"fill":979,"stroke":980,"style":1048},"124.0",[121,1444,1446],{"x":1369,"y":1445,"style":1371},"146.0","E04",[92,1448],{"x":1335,"y":1442,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1450,1452],{"transform":1451},"translate(148.0,132.0) scale(0.750)",[114,1453],{"d":222,"fill":117,"stroke":139,"style":1380},[92,1455],{"x":1345,"y":1442,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1457,1459],{"transform":1458},"translate(314.6,132.0) scale(0.750)",[114,1460],{"d":222,"fill":117,"stroke":139,"style":1380},[92,1462],{"x":1352,"y":1442,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1464,1466],{"transform":1465},"translate(482.1,133.0) scale(0.667)",[114,1467],{"d":1437,"fill":117,"stroke":1438,"style":1439},[92,1469],{"x":1358,"y":1442,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1471,1473],{"transform":1472},"translate(648.7,133.0) scale(0.667)",[114,1474],{"d":1437,"fill":117,"stroke":1438,"style":1439},[92,1476],{"x":134,"y":1477,"width":1331,"height":1366,"rx":95,"fill":979,"stroke":980,"style":1048},"158.0",[121,1479,1481],{"x":1369,"y":1480,"style":1371},"180.0","E06",[92,1483],{"x":1335,"y":1477,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1485,1487],{"transform":1486},"translate(148.0,166.0) scale(0.750)",[114,1488],{"d":222,"fill":117,"stroke":139,"style":1380},[92,1490],{"x":1345,"y":1477,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1492,1494],{"transform":1493},"translate(315.6,167.0) scale(0.667)",[114,1495],{"d":1437,"fill":117,"stroke":1438,"style":1439},[92,1497],{"x":1352,"y":1477,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1499,1501],{"transform":1500},"translate(481.1,166.0) scale(0.750)",[114,1502],{"d":222,"fill":117,"stroke":139,"style":1380},[92,1504],{"x":1358,"y":1477,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1506,1508],{"transform":1507},"translate(647.7,166.0) scale(0.750)",[114,1509],{"d":222,"fill":117,"stroke":139,"style":1380},[92,1511],{"x":134,"y":1512,"width":1331,"height":1366,"rx":95,"fill":979,"stroke":980,"style":1048},"192.0",[121,1514,1516],{"x":1369,"y":1515,"style":1371},"214.0","E07",[92,1518],{"x":1335,"y":1512,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1520,1522],{"transform":1521},"translate(149.0,201.0) scale(0.667)",[114,1523],{"d":1437,"fill":117,"stroke":1438,"style":1439},[92,1525],{"x":1345,"y":1512,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1527,1529],{"transform":1528},"translate(314.6,200.0) scale(0.750)",[114,1530],{"d":222,"fill":117,"stroke":139,"style":1380},[92,1532],{"x":1352,"y":1512,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1534,1536],{"transform":1535},"translate(482.1,201.0) scale(0.667)",[114,1537],{"d":1437,"fill":117,"stroke":1438,"style":1439},[92,1539],{"x":1358,"y":1512,"width":1336,"height":1366,"rx":95,"fill":98,"stroke":980,"style":1048},[110,1541,1543],{"transform":1542},"translate(648.7,201.0) scale(0.667)",[114,1544],{"d":1437,"fill":117,"stroke":1438,"style":1439},[25,1546,1548],{"id":1547},"step-5-an-alternative-concat-on-the-key","Step 5 — An alternative: concat on the key",[10,1550,1551,1552,1555,1556,1559],{},"When every file has already been reduced to one row per key, ",[14,1553,1554],{},"pd.concat"," with ",[14,1557,1558],{},"axis=1"," aligns them on the index in one call, with no chain:",[30,1561,1563],{"className":245,"code":1562,"language":247,"meta":35,"style":35},"indexed = [df.set_index(\"EmployeeID\") for df in prefixed]\nside_by_side = pd.concat(indexed, axis=1, join=\"outer\").sort_index()\nprint(side_by_side.shape, side_by_side.index.tolist())\n",[14,1564,1565,1589,1617],{"__ignoreMap":35},[39,1566,1567,1570,1572,1574,1576,1579,1581,1584,1586],{"class":41,"line":42},[39,1568,1569],{"class":258},"indexed ",[39,1571,302],{"class":254},[39,1573,812],{"class":258},[39,1575,305],{"class":49},[39,1577,1578],{"class":258},") ",[39,1580,346],{"class":254},[39,1582,1583],{"class":258}," df ",[39,1585,352],{"class":254},[39,1587,1588],{"class":258}," prefixed]\n",[39,1590,1591,1594,1596,1599,1601,1603,1605,1607,1610,1612,1614],{"class":41,"line":268},[39,1592,1593],{"class":258},"side_by_side ",[39,1595,302],{"class":254},[39,1597,1598],{"class":258}," pd.concat(indexed, ",[39,1600,1270],{"class":322},[39,1602,302],{"class":254},[39,1604,1275],{"class":328},[39,1606,367],{"class":258},[39,1608,1609],{"class":322},"join",[39,1611,302],{"class":254},[39,1613,883],{"class":49},[39,1615,1616],{"class":258},").sort_index()\n",[39,1618,1619,1621],{"class":41,"line":282},[39,1620,725],{"class":328},[39,1622,1623],{"class":258},"(side_by_side.shape, side_by_side.index.tolist())\n",[10,1625,1626,1627,1630,1631,1633],{},"It produces the same wide table as the outer merges. ",[14,1628,1629],{},"concat"," raises on duplicate index values with an \"InvalidIndexError\", which acts as a uniqueness check of sorts, but it does not produce as clear a message as ",[14,1632,758],{},", so keep the explicit duplicate check from Step 2 either way.",[25,1635,1637],{"id":1636},"step-6-write-the-result-with-a-coverage-summary","Step 6 — Write the result with a coverage summary",[10,1639,1640,1641,1644],{},"Convert the numeric columns — the laptop count became a float during the outer merge, because missing values force integers to float, and the nullable ",[14,1642,1643],{},"Int64"," type turns it back into whole numbers with blanks — then write the wide table and a summary of how many employees each source covers:",[30,1646,1648],{"className":245,"code":1647,"language":247,"meta":35,"style":35},"wide[\"payroll.Salary\"] = pd.to_numeric(wide[\"payroll.Salary\"])\nwide[\"training.CoursesCompleted\"] = pd.to_numeric(wide[\"training.CoursesCompleted\"]).astype(\"Int64\")\nwide[\"it_assets.Laptops\"] = wide[\"it_assets.Laptops\"].astype(\"Int64\")\ncoverage = wide[flags].sum().rename(\"Employees\").to_frame()\ncoverage[\"Missing\"] = len(wide) - coverage[\"Employees\"]\nprint(coverage)\n\nwith pd.ExcelWriter(\"employees_combined.xlsx\", engine=\"xlsxwriter\") as xw:\n    wide.drop(columns=flags).to_excel(xw, sheet_name=\"Combined\", index=False)\n    coverage.to_excel(xw, sheet_name=\"Coverage\")\n    wide.loc[wide[\"Sources\"] \u003C len(sources), [\"EmployeeID\"] + flags].to_excel(xw, sheet_name=\"Gaps\", index=False)\n",[14,1649,1650,1669,1692,1715,1731,1759,1766,1770,1798,1828,1842],{"__ignoreMap":35},[39,1651,1652,1654,1657,1659,1661,1664,1666],{"class":41,"line":42},[39,1653,1257],{"class":258},[39,1655,1656],{"class":49},"\"payroll.Salary\"",[39,1658,657],{"class":258},[39,1660,302],{"class":254},[39,1662,1663],{"class":258}," pd.to_numeric(wide[",[39,1665,1656],{"class":49},[39,1667,1668],{"class":258},"])\n",[39,1670,1671,1673,1676,1678,1680,1682,1684,1687,1690],{"class":41,"line":268},[39,1672,1257],{"class":258},[39,1674,1675],{"class":49},"\"training.CoursesCompleted\"",[39,1677,657],{"class":258},[39,1679,302],{"class":254},[39,1681,1663],{"class":258},[39,1683,1675],{"class":49},[39,1685,1686],{"class":258},"]).astype(",[39,1688,1689],{"class":49},"\"Int64\"",[39,1691,332],{"class":258},[39,1693,1694,1696,1699,1701,1703,1706,1708,1711,1713],{"class":41,"line":282},[39,1695,1257],{"class":258},[39,1697,1698],{"class":49},"\"it_assets.Laptops\"",[39,1700,657],{"class":258},[39,1702,302],{"class":254},[39,1704,1705],{"class":258}," wide[",[39,1707,1698],{"class":49},[39,1709,1710],{"class":258},"].astype(",[39,1712,1689],{"class":49},[39,1714,332],{"class":258},[39,1716,1717,1720,1722,1725,1728],{"class":41,"line":289},[39,1718,1719],{"class":258},"coverage ",[39,1721,302],{"class":254},[39,1723,1724],{"class":258}," wide[flags].sum().rename(",[39,1726,1727],{"class":49},"\"Employees\"",[39,1729,1730],{"class":258},").to_frame()\n",[39,1732,1733,1736,1739,1741,1743,1745,1748,1751,1754,1756],{"class":41,"line":311},[39,1734,1735],{"class":258},"coverage[",[39,1737,1738],{"class":49},"\"Missing\"",[39,1740,657],{"class":258},[39,1742,302],{"class":254},[39,1744,597],{"class":328},[39,1746,1747],{"class":258},"(wide) ",[39,1749,1750],{"class":254},"-",[39,1752,1753],{"class":258}," coverage[",[39,1755,1727],{"class":49},[39,1757,1758],{"class":258},"]\n",[39,1760,1761,1763],{"class":41,"line":335},[39,1762,725],{"class":328},[39,1764,1765],{"class":258},"(coverage)\n",[39,1767,1768],{"class":41,"line":382},[39,1769,286],{"emptyLinePlaceholder":285},[39,1771,1772,1775,1778,1781,1783,1786,1788,1791,1793,1795],{"class":41,"line":400},[39,1773,1774],{"class":254},"with",[39,1776,1777],{"class":258}," pd.ExcelWriter(",[39,1779,1780],{"class":49},"\"employees_combined.xlsx\"",[39,1782,367],{"class":258},[39,1784,1785],{"class":322},"engine",[39,1787,302],{"class":254},[39,1789,1790],{"class":49},"\"xlsxwriter\"",[39,1792,1578],{"class":258},[39,1794,276],{"class":254},[39,1796,1797],{"class":258}," xw:\n",[39,1799,1800,1803,1805,1807,1810,1813,1815,1818,1820,1822,1824,1826],{"class":41,"line":411},[39,1801,1802],{"class":258},"    wide.drop(",[39,1804,392],{"class":322},[39,1806,302],{"class":254},[39,1808,1809],{"class":258},"flags).to_excel(xw, ",[39,1811,1812],{"class":322},"sheet_name",[39,1814,302],{"class":254},[39,1816,1817],{"class":49},"\"Combined\"",[39,1819,367],{"class":258},[39,1821,629],{"class":322},[39,1823,302],{"class":254},[39,1825,586],{"class":328},[39,1827,332],{"class":258},[39,1829,1830,1833,1835,1837,1840],{"class":41,"line":420},[39,1831,1832],{"class":258},"    coverage.to_excel(xw, ",[39,1834,1812],{"class":322},[39,1836,302],{"class":254},[39,1838,1839],{"class":49},"\"Coverage\"",[39,1841,332],{"class":258},[39,1843,1844,1847,1849,1851,1854,1856,1859,1861,1863,1865,1868,1870,1872,1875,1877,1879,1881,1883],{"class":41,"line":425},[39,1845,1846],{"class":258},"    wide.loc[wide[",[39,1848,1260],{"class":49},[39,1850,657],{"class":258},[39,1852,1853],{"class":254},"\u003C",[39,1855,597],{"class":328},[39,1857,1858],{"class":258},"(sources), [",[39,1860,305],{"class":49},[39,1862,657],{"class":258},[39,1864,1022],{"class":254},[39,1866,1867],{"class":258}," flags].to_excel(xw, ",[39,1869,1812],{"class":322},[39,1871,302],{"class":254},[39,1873,1874],{"class":49},"\"Gaps\"",[39,1876,367],{"class":258},[39,1878,629],{"class":322},[39,1880,302],{"class":254},[39,1882,586],{"class":328},[39,1884,332],{"class":258},[10,1886,59,1887,1890],{},[14,1888,1889],{},"Gaps"," sheet lists every employee missing from at least one source, with the flags showing which — the list someone needs to work through before the combined data is trusted.",[25,1892,1894],{"id":1893},"performance-notes","Performance notes",[10,1896,1897,1898,1901],{},"Each merge in the chain is a hash join, fast even for hundreds of thousands of keys; the reading of the workbooks takes far longer than the merging. With a dozen or more sources, merge order does not change the result of outer joins, but merging the largest files last keeps the intermediate frames smaller for longer. When the files are huge, reading only the columns you need with ",[14,1899,1900],{},"usecols"," saves more time than anything done at merge time.",[25,1903,1905],{"id":1904},"common-pitfalls","Common pitfalls",[1907,1908,1909,1926,1942,1951,1963],"ul",{},[1910,1911,1912,1916,1917,367,1919,773,1922,1925],"li",{},[1913,1914,1915],"strong",{},"Keys that look equal but are not."," ",[14,1918,1372],{},[14,1920,1921],{},"e01",[14,1923,1924],{},"E01 "," are three keys to pandas; normalise before merging.",[1910,1927,1928,1916,1931,1933,1934,1937,1938,1941],{},[1913,1929,1930],{},"Inner joins by default.",[14,1932,16],{}," defaults to ",[14,1935,1936],{},"how=\"inner\"",", which drops anyone missing from any file. Use ",[14,1939,1940],{},"outer"," and decide explicitly.",[1910,1943,1944,1947,1948,1950],{},[1913,1945,1946],{},"Duplicate keys."," They multiply rows across every later merge; check per file and use ",[14,1949,951],{},".",[1910,1952,1953,1962],{},[1913,1954,1955,773,1958,1961],{},[14,1956,1957],{},"_x",[14,1959,1960],{},"_y"," suffixes."," Prefix columns with their source before merging.",[1910,1964,1965,1968,1969,1971,1972,1974],{},[1913,1966,1967],{},"Reading IDs as numbers."," Leading zeros disappear and ",[14,1970,537],{}," no longer matches ",[14,1973,541],{},"; read keys as text.",[25,1976,1978],{"id":1977},"conclusion","Conclusion",[10,1980,1981,1982,1985,1986,1988],{},"Load every file through one function that normalises the key's name, type and case; check each file for duplicate keys and aggregate or reject them; prefix columns with their source; and merge in a ",[14,1983,1984],{},"reduce"," loop with outer joins and ",[14,1987,951],{},". Add a presence flag per source to tell missing records from blank fields, and write the combined table with a coverage summary and a list of gaps.",[25,1990,1992],{"id":1991},"frequently-asked-questions","Frequently asked questions",[10,1994,1995,1998],{},[1913,1996,1997],{},"How do I merge more than two DataFrames in pandas?","\nPut them in a list and use functools.reduce(lambda a, b: a.merge(b, on=\"key\", how=\"outer\"), frames). It chains the merges for any number of files. pd.concat(frames, axis=1) also works when each frame is indexed by the key and has unique keys.",[10,2000,2001,2004],{},[1913,2002,2003],{},"How do I avoid _x and _y columns when merging?","\nRename or prefix the non-key columns before merging, for example with df.add_prefix(\"payroll.\") after setting the key as the index. Then every column name says which file it came from.",[10,2006,2007,2010],{},[1913,2008,2009],{},"Why did my merged table get more rows than any input?","\nAt least one file has duplicate values in the key column, so each duplicate matches every row for that key in the other files. Check with df.duplicated(\"key\") and use validate=\"one_to_one\" to make merges fail on duplicates.",[10,2012,2013,2016],{},[1913,2014,2015],{},"How do I know which file each row came from?","\nAdd a True flag column per file before merging and fill the missing flags with False afterwards. The flags show which sources contain each key, separately from whether their fields are blank.",[25,2018,2020],{"id":2019},"related","Related",[1907,2022,2023,2029,2036,2043],{},[1910,2024,2025,2026,2028],{},"Up one level: ",[19,2027,22],{"href":21}," — every way to combine tables.",[1910,2030,2031,2035],{},[19,2032,2034],{"href":2033},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-two-excel-files-on-common-column-python\u002F","Merge Two Excel Files on a Common Column in Python"," — the two-file case in detail.",[1910,2037,2038,2042],{},[19,2039,2041],{"href":2040},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-excel-files-with-different-column-names\u002F","Merge Excel Files with Different Column Names"," — mapping headers before joining.",[1910,2044,2045,2049],{},[19,2046,2048],{"href":2047},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fcombine-multiple-excel-files-into-one-python\u002F","Combine Multiple Excel Files into One"," — stacking files with the same columns instead.",[2051,2052,2053],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}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}",{"title":35,"searchDepth":268,"depth":268,"links":2055},[2056,2057,2058,2059,2060,2061,2062,2063,2064,2065,2066,2067],{"id":27,"depth":268,"text":28},{"id":238,"depth":268,"text":239},{"id":545,"depth":268,"text":546},{"id":762,"depth":268,"text":763},{"id":1129,"depth":268,"text":1130},{"id":1547,"depth":268,"text":1548},{"id":1636,"depth":268,"text":1637},{"id":1893,"depth":268,"text":1894},{"id":1904,"depth":268,"text":1905},{"id":1977,"depth":268,"text":1978},{"id":1991,"depth":268,"text":1992},{"id":2019,"depth":268,"text":2020},"2026-09-29","Combine several workbooks that describe the same records — HR, payroll, IT and training exports keyed by employee ID — into one wide table: chain merges with functools.reduce, keep keys that appear in only some files, avoid _x\u002F_y column collisions, record which files each key came from, catch duplicate keys before they multiply rows, and write the result with a coverage summary.","md",[2072,2074,2076,2078],{"q":1997,"a":2073},"Put them in a list and use functools.reduce(lambda a, b: a.merge(b, on=\"key\", how=\"outer\"), frames). It chains the merges for any number of files. pd.concat(frames, axis=1) also works when each frame is indexed by the key and has unique keys.",{"q":2003,"a":2075},"Rename or prefix the non-key columns before merging, for example with df.add_prefix(\"payroll.\") after setting the key as the index. Then every column name says which file it came from.",{"q":2009,"a":2077},"At least one file has duplicate values in the key column, so each duplicate matches every row for that key in the other files. Check with df.duplicated(\"key\") and use validate=\"one_to_one\" to make merges fail on duplicates.",{"q":2015,"a":2079},"Add a True flag column per file before merging and fill the missing flags with False afterwards. The flags show which sources contain each key, separately from whether their fields are blank.","merge more than two excel files on a key column",null,{"breadcrumb":2083},[2084,2087,2090],{"name":2085,"item":2086},"Home","\u002F",{"name":2088,"item":2089},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":2091,"item":21},"Merging and Joining Excel DataFrames with Pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-more-than-two-excel-files-on-a-key-column",{"title":2094,"description":2095},"Merge Multiple Excel Files on a Common Key with pandas","Merge three or more Excel files on an ID column in pandas: reduce over merge, outer joins, prefixes instead of _x\u002F_y suffixes, validate=\"one_to_one\", a per-file presence flag, pd.concat(axis=1) on the key, and a coverage report.","merge-more-than-two-excel-files-on-a-key-column","advanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-more-than-two-excel-files-on-a-key-column\u002Findex",[2099,2100],"joins-and-lookups","sheets-and-files","how-to","QxrhOIrMjqMD_Lwagg2vBt98RJPuk-8ozTCHazB_nhc",[2104,2108],{"title":2105,"path":2106,"stem":2107,"children":-1},"Merge Excel Files with Different Column Names in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-excel-files-with-different-column-names","advanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-excel-files-with-different-column-names\u002Findex",{"title":2109,"path":2110,"stem":2111,"children":-1},"Merge on Keys That Differ in Case and Spacing with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-on-keys-that-differ-in-case-and-spacing-with-pandas","advanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fmerge-on-keys-that-differ-in-case-and-spacing-with-pandas\u002Findex",[2113,2115],{"path":2114,"title":2088},"\u002Fadvanced-data-transformation-and-cleaning",{"path":2116,"title":2091},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes",[],[2119,2122,2125,2128,2131,2134],{"path":2120,"title":2121,"kind":2101},"\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":2123,"title":2124,"kind":2101},"\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":2126,"title":2127,"kind":2101},"\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":2129,"title":2130,"kind":2101},"\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":2132,"title":2133,"kind":2101},"\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":2135,"title":2136,"kind":2101},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fapproximate-match-vlookup-in-pandas","Approximate-Match VLOOKUP for Tiered Rates in pandas",1790972096394]