[{"data":1,"prerenderedAt":1688},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fcoalesce-several-excel-columns-into-one-with-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fcoalesce-several-excel-columns-into-one-with-pandas":1658,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fcoalesce-several-excel-columns-into-one-with-pandas":1667,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fcoalesce-several-excel-columns-into-one-with-pandas":1672,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fcoalesce-several-excel-columns-into-one-with-pandas":1673},{"id":4,"title":5,"body":6,"dateModified":1622,"datePublished":1622,"description":1623,"extension":1624,"faq":1625,"keyword":1634,"kind":1635,"meta":1636,"navigation":140,"path":1646,"seo":1647,"slug":1650,"stem":1651,"topics":1652,"type":1656,"__hash__":1657},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fcoalesce-several-excel-columns-into-one-with-pandas\u002Findex.md","Coalesce Several Excel Columns into One with pandas",{"type":7,"value":8,"toc":1608},"minimark",[9,56,61,89,108,112,280,310,531,535,538,706,722,726,733,784,801,805,812,901,918,999,1003,1006,1157,1164,1168,1244,1258,1409,1413,1437,1448,1452,1511,1515,1529,1533,1547,1556,1565,1571,1575,1604],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,37,38,41,42,45,46,49,50,55],"p",{},"Data merged from several systems often has the same fact in several columns, each filled in some of the time. A contact list has ",[14,15,16],"code",{},"Mobile",", ",[14,19,20],{},"Work phone"," and ",[14,23,24],{},"Home phone",", and the report needs one ",[14,27,28],{},"Phone"," — the mobile if there is one, else the work number, else home. A product table has a ",[14,31,32],{},"Price (2026 list)",", a ",[14,35,36],{},"Price (2025 list)"," and a ",[14,39,40],{},"Manual price",". In SQL this is ",[14,43,44],{},"COALESCE(mobile, work, home)","; in Excel it is a nested ",[14,47,48],{},"IF(A2\u003C>\"\", A2, IF(B2\u003C>\"\", B2, C2))"," that grows with every new column. pandas does it in one line — once the \"empty\" values have been made genuinely empty, which in spreadsheet data is most of the work. This page covers the one-liner, the clean-up, recording where each value came from and catching sources that disagree. It belongs to ",[51,52,54],"a",{"href":53},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002F","Cleaning Excel Data with pandas",".",[57,58,60],"h2",{"id":59},"prerequisites","Prerequisites",[62,63,68],"pre",{"className":64,"code":65,"language":66,"meta":67,"style":67},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,69,70],{"__ignoreMap":67},[71,72,75,79,83,86],"span",{"class":73,"line":74},"line",1,[71,76,78],{"class":77},"sMTad","pip",[71,80,82],{"class":81},"srMev"," install",[71,84,85],{"class":81}," pandas",[71,87,88],{"class":81}," openpyxl\n",[10,90,91,94,95,99,100,103,104,107],{},[14,92,93],{},"contacts.xlsx"," has six contacts with three phone columns. Some cells are truly empty; others only ",[96,97,98],"em",{},"look"," empty — a cell with two spaces, a cell with ",[14,101,102],{},"n\u002Fa",", a cell with ",[14,105,106],{},"-"," — the leftovers of hand-maintained sheets and system exports.",[57,109,111],{"id":110},"step-1-first-non-empty-value-with-bfill","Step 1 — First non-empty value with bfill",[62,113,117],{"className":114,"code":115,"language":116,"meta":67,"style":67},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\ncontacts = pd.read_excel(\"contacts.xlsx\", dtype=str, keep_default_na=False)\norder = [\"Mobile\", \"Work phone\", \"Home phone\"]\nnaive = contacts[order].replace(\"\", pd.NA).bfill(axis=1).iloc[:, 0]\nprint(contacts[[\"Name\"]].assign(Phone=naive).to_string(index=False))\n","python",[14,118,119,135,142,182,209,248],{"__ignoreMap":67},[71,120,121,125,129,132],{"class":73,"line":74},[71,122,124],{"class":123},"s-kum","import",[71,126,128],{"class":127},"skGVy"," pandas ",[71,130,131],{"class":123},"as",[71,133,134],{"class":127}," pd\n",[71,136,138],{"class":73,"line":137},2,[71,139,141],{"emptyLinePlaceholder":140},true,"\n",[71,143,145,148,151,154,157,159,163,165,169,171,174,176,179],{"class":73,"line":144},3,[71,146,147],{"class":127},"contacts ",[71,149,150],{"class":123},"=",[71,152,153],{"class":127}," pd.read_excel(",[71,155,156],{"class":81},"\"contacts.xlsx\"",[71,158,17],{"class":127},[71,160,162],{"class":161},"sa561","dtype",[71,164,150],{"class":123},[71,166,168],{"class":167},"sP0c6","str",[71,170,17],{"class":127},[71,172,173],{"class":161},"keep_default_na",[71,175,150],{"class":123},[71,177,178],{"class":167},"False",[71,180,181],{"class":127},")\n",[71,183,185,188,190,193,196,198,201,203,206],{"class":73,"line":184},4,[71,186,187],{"class":127},"order ",[71,189,150],{"class":123},[71,191,192],{"class":127}," [",[71,194,195],{"class":81},"\"Mobile\"",[71,197,17],{"class":127},[71,199,200],{"class":81},"\"Work phone\"",[71,202,17],{"class":127},[71,204,205],{"class":81},"\"Home phone\"",[71,207,208],{"class":127},"]\n",[71,210,212,215,217,220,223,226,229,232,235,237,240,243,246],{"class":73,"line":211},5,[71,213,214],{"class":127},"naive ",[71,216,150],{"class":123},[71,218,219],{"class":127}," contacts[order].replace(",[71,221,222],{"class":81},"\"\"",[71,224,225],{"class":127},", pd.",[71,227,228],{"class":167},"NA",[71,230,231],{"class":127},").bfill(",[71,233,234],{"class":161},"axis",[71,236,150],{"class":123},[71,238,239],{"class":167},"1",[71,241,242],{"class":127},").iloc[:, ",[71,244,245],{"class":167},"0",[71,247,208],{"class":127},[71,249,251,254,257,260,263,265,267,270,273,275,277],{"class":73,"line":250},6,[71,252,253],{"class":167},"print",[71,255,256],{"class":127},"(contacts[[",[71,258,259],{"class":81},"\"Name\"",[71,261,262],{"class":127},"]].assign(",[71,264,28],{"class":161},[71,266,150],{"class":123},[71,268,269],{"class":127},"naive).to_string(",[71,271,272],{"class":161},"index",[71,274,150],{"class":123},[71,276,178],{"class":167},[71,278,279],{"class":127},"))\n",[10,281,282,285,286,289,290,293,294,297,298,302,303,306,307,309],{},[14,283,284],{},"bfill(axis=1)"," fills each missing value from the next column to its right, so after the fill the first column holds the first non-missing value of the row; ",[14,287,288],{},".iloc[:, 0]"," takes it. The column ",[96,291,292],{},"order"," is the priority. Reading with ",[14,295,296],{},"dtype=str"," keeps phone numbers as text, so leading zeros survive — see ",[51,299,301],{"href":300},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fkeep-leading-zeros-when-reading-excel-with-pandas\u002F","Keep Leading Zeros When Reading Excel with pandas",". The result is almost right: Claire gets ",[14,304,305],{},"\"  \""," (two spaces) instead of her home number, and Ken gets ",[14,308,102],{},", because those cells were not empty strings and so were not treated as missing.",[311,312,320,321,320,325,320,329,320,336,320,345,320,349,320,356,320,360,320,364,320,368,320,372,320,376,320,380,320,385,320,388,320,390,320,392,320,394,320,396,320,399,320,403,320,407,320,411,320,413,320,417,320,419,320,422,320,426,320,428,320,430,320,433,320,435,320,438,320,441,320,444,320,446,320,448,320,450,320,453,320,456,320,460,320,463,320,465,320,468,320,470,320,472,320,474,320,476,320,478,320,480,320,482,320,484,320,486,320,488,320,490,320,492,320,494,320,496,320,502,320,506,320,512,320,517,320,525],"svg",{"viewBox":313,"role":314,"ariaLabelledBy":315,"xmlns":318,"style":319},"4 0 752 192","img",[316,317],"co-bfill-t","co-bfill-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  ",[322,323,324],"title",{"id":316},"The first non-empty phone number per contact",[326,327,328],"desc",{"id":317},"On the left the Mobile, Work phone and Home phone columns with gaps. On the right one Phone column holding the mobile where present, otherwise the work number, otherwise the home number.",[330,331],"rect",{"x":332,"y":245,"width":333,"height":334,"fill":335},"4","752","192","#ffffff",[330,337],{"x":338,"y":339,"width":340,"height":341,"rx":245,"fill":342,"stroke":343,"style":344},"20.0","40.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[330,346],{"x":347,"y":339,"width":348,"height":341,"rx":245,"fill":342,"stroke":343,"style":344},"52.0","87.6",[350,351,355],"text",{"x":352,"y":353,"style":354},"95.8","57.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[330,357],{"x":358,"y":339,"width":359,"height":341,"rx":245,"fill":342,"stroke":343,"style":344},"139.6","93.2",[350,361,363],{"x":362,"y":353,"style":354},"186.2","B",[330,365],{"x":366,"y":339,"width":367,"height":341,"rx":245,"fill":342,"stroke":343,"style":344},"232.8","97.1",[350,369,371],{"x":370,"y":353,"style":354},"281.3","C",[330,373],{"x":338,"y":374,"width":340,"height":375,"rx":245,"fill":342,"stroke":343,"style":344},"64.0","28.0",[350,377,239],{"x":378,"y":379,"style":354},"36.0","83.0",[330,381],{"x":347,"y":374,"width":348,"height":375,"rx":245,"fill":382,"stroke":383,"style":384},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[350,386,16],{"x":352,"y":379,"style":387},"font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[330,389],{"x":358,"y":374,"width":359,"height":375,"rx":245,"fill":382,"stroke":383,"style":384},[350,391,20],{"x":362,"y":379,"style":387},[330,393],{"x":366,"y":374,"width":367,"height":375,"rx":245,"fill":382,"stroke":383,"style":384},[350,395,24],{"x":370,"y":379,"style":387},[330,397],{"x":338,"y":398,"width":340,"height":375,"rx":245,"fill":342,"stroke":343,"style":344},"92.0",[350,400,402],{"x":378,"y":401,"style":354},"111.0","2",[330,404],{"x":347,"y":398,"width":348,"height":375,"rx":245,"fill":405,"stroke":406,"style":384},"#d9f4f1","var(--teal,#0f9488)",[350,408,410],{"x":352,"y":401,"style":409},"font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","07700 900111",[330,412],{"x":358,"y":398,"width":359,"height":375,"rx":245,"fill":335,"stroke":343,"style":344},[350,414,416],{"x":362,"y":401,"style":415},"font-size:10.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","0161 496 0001",[330,418],{"x":366,"y":398,"width":367,"height":375,"rx":245,"fill":335,"stroke":343,"style":344},[330,420],{"x":338,"y":421,"width":340,"height":375,"rx":245,"fill":342,"stroke":343,"style":344},"120.0",[350,423,425],{"x":378,"y":424,"style":354},"139.0","3",[330,427],{"x":347,"y":421,"width":348,"height":375,"rx":245,"fill":335,"stroke":343,"style":344},[330,429],{"x":358,"y":421,"width":359,"height":375,"rx":245,"fill":405,"stroke":406,"style":384},[350,431,432],{"x":362,"y":424,"style":409},"0113 496 0002",[330,434],{"x":366,"y":421,"width":367,"height":375,"rx":245,"fill":335,"stroke":343,"style":344},[350,436,437],{"x":370,"y":424,"style":415},"0113 496 0102",[330,439],{"x":338,"y":440,"width":340,"height":375,"rx":245,"fill":342,"stroke":343,"style":344},"148.0",[350,442,332],{"x":378,"y":443,"style":354},"167.0",[330,445],{"x":347,"y":440,"width":348,"height":375,"rx":245,"fill":335,"stroke":343,"style":344},[330,447],{"x":358,"y":440,"width":359,"height":375,"rx":245,"fill":335,"stroke":343,"style":344},[330,449],{"x":366,"y":440,"width":367,"height":375,"rx":245,"fill":405,"stroke":406,"style":384},[350,451,452],{"x":370,"y":443,"style":409},"020 7946 0003",[330,454],{"x":455,"y":339,"width":340,"height":341,"rx":245,"fill":342,"stroke":343,"style":344},"612.9",[330,457],{"x":458,"y":339,"width":459,"height":341,"rx":245,"fill":342,"stroke":343,"style":344},"644.9","95.1",[350,461,355],{"x":462,"y":353,"style":354},"692.5",[330,464],{"x":455,"y":374,"width":340,"height":375,"rx":245,"fill":342,"stroke":343,"style":344},[350,466,239],{"x":467,"y":379,"style":354},"628.9",[330,469],{"x":458,"y":374,"width":459,"height":375,"rx":245,"fill":382,"stroke":383,"style":384},[350,471,28],{"x":462,"y":379,"style":387},[330,473],{"x":455,"y":398,"width":340,"height":375,"rx":245,"fill":342,"stroke":343,"style":344},[350,475,402],{"x":467,"y":401,"style":354},[330,477],{"x":458,"y":398,"width":459,"height":375,"rx":245,"fill":405,"stroke":406,"style":384},[350,479,410],{"x":462,"y":401,"style":409},[330,481],{"x":455,"y":421,"width":340,"height":375,"rx":245,"fill":342,"stroke":343,"style":344},[350,483,425],{"x":467,"y":424,"style":354},[330,485],{"x":458,"y":421,"width":459,"height":375,"rx":245,"fill":405,"stroke":406,"style":384},[350,487,432],{"x":462,"y":424,"style":409},[330,489],{"x":455,"y":440,"width":340,"height":375,"rx":245,"fill":342,"stroke":343,"style":344},[350,491,332],{"x":467,"y":443,"style":354},[330,493],{"x":458,"y":440,"width":459,"height":375,"rx":245,"fill":405,"stroke":406,"style":384},[350,495,452],{"x":462,"y":443,"style":409},[350,497,501],{"x":498,"y":499,"style":500},"174.9","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before",[350,503,505],{"x":504,"y":499,"style":500},"676.5","after",[73,507],{"x1":508,"y1":509,"x2":510,"y2":509,"stroke":383,"style":511},"339.9","108.0","598.9","stroke-width:2px",[513,514],"polygon",{"points":515,"fill":516},"602.9,108.0 593.9,113.0 593.9,103.0","#5b5cf0",[330,518],{"x":519,"y":520,"width":521,"height":522,"rx":523,"fill":335,"stroke":343,"style":524},"374.2","75.0","194.5","22.0","11","stroke-width:1.2px",[350,526,530],{"x":527,"y":528,"style":529},"471.4","90.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","bfill(axis=1).iloc[:, 0]",[57,532,534],{"id":533},"step-2-decide-what-counts-as-empty","Step 2 — Decide what counts as empty",[10,536,537],{},"Before coalescing, turn everything that means \"no value\" into a real missing value. Strip whitespace, then replace the placeholders your data uses:",[62,539,541],{"className":114,"code":540,"language":116,"meta":67,"style":67},"PLACEHOLDERS = {\"\", \"-\", \"n\u002Fa\", \"na\", \"none\", \"null\", \"unknown\", \"tbc\"}\n\ndef as_missing(s):\n    s = s.astype(\"string\").str.strip()\n    return s.mask(s.str.lower().isin(PLACEHOLDERS))\n\nphones = contacts[order].apply(as_missing)\ncontacts[\"Phone\"] = phones.bfill(axis=1).iloc[:, 0]\nprint(contacts[[\"Name\", \"Phone\"]].to_string(index=False))\n",[14,542,543,594,598,610,626,638,642,653,682],{"__ignoreMap":67},[71,544,545,548,551,554,556,558,561,563,566,568,571,573,576,578,581,583,586,588,591],{"class":73,"line":74},[71,546,547],{"class":167},"PLACEHOLDERS",[71,549,550],{"class":123}," =",[71,552,553],{"class":127}," {",[71,555,222],{"class":81},[71,557,17],{"class":127},[71,559,560],{"class":81},"\"-\"",[71,562,17],{"class":127},[71,564,565],{"class":81},"\"n\u002Fa\"",[71,567,17],{"class":127},[71,569,570],{"class":81},"\"na\"",[71,572,17],{"class":127},[71,574,575],{"class":81},"\"none\"",[71,577,17],{"class":127},[71,579,580],{"class":81},"\"null\"",[71,582,17],{"class":127},[71,584,585],{"class":81},"\"unknown\"",[71,587,17],{"class":127},[71,589,590],{"class":81},"\"tbc\"",[71,592,593],{"class":127},"}\n",[71,595,596],{"class":73,"line":137},[71,597,141],{"emptyLinePlaceholder":140},[71,599,600,603,607],{"class":73,"line":144},[71,601,602],{"class":123},"def",[71,604,606],{"class":605},"s_Opv"," as_missing",[71,608,609],{"class":127},"(s):\n",[71,611,612,615,617,620,623],{"class":73,"line":184},[71,613,614],{"class":127},"    s ",[71,616,150],{"class":123},[71,618,619],{"class":127}," s.astype(",[71,621,622],{"class":81},"\"string\"",[71,624,625],{"class":127},").str.strip()\n",[71,627,628,631,634,636],{"class":73,"line":211},[71,629,630],{"class":123},"    return",[71,632,633],{"class":127}," s.mask(s.str.lower().isin(",[71,635,547],{"class":167},[71,637,279],{"class":127},[71,639,640],{"class":73,"line":250},[71,641,141],{"emptyLinePlaceholder":140},[71,643,645,648,650],{"class":73,"line":644},7,[71,646,647],{"class":127},"phones ",[71,649,150],{"class":123},[71,651,652],{"class":127}," contacts[order].apply(as_missing)\n",[71,654,656,659,662,665,667,670,672,674,676,678,680],{"class":73,"line":655},8,[71,657,658],{"class":127},"contacts[",[71,660,661],{"class":81},"\"Phone\"",[71,663,664],{"class":127},"] ",[71,666,150],{"class":123},[71,668,669],{"class":127}," phones.bfill(",[71,671,234],{"class":161},[71,673,150],{"class":123},[71,675,239],{"class":167},[71,677,242],{"class":127},[71,679,245],{"class":167},[71,681,208],{"class":127},[71,683,685,687,689,691,693,695,698,700,702,704],{"class":73,"line":684},9,[71,686,253],{"class":167},[71,688,256],{"class":127},[71,690,259],{"class":81},[71,692,17],{"class":127},[71,694,661],{"class":81},[71,696,697],{"class":127},"]].to_string(",[71,699,272],{"class":161},[71,701,150],{"class":123},[71,703,178],{"class":167},[71,705,279],{"class":127},[10,707,708,709,21,711,713,714,717,718,55],{},"Now Claire's two spaces are missing, so her home number is used; Ken has ",[14,710,102],{},[14,712,106],{}," in two columns and nothing in the third, so his phone is honestly ",[14,715,716],{},"\u003CNA>"," — no number at all, which the report should show as such rather than printing \"n\u002Fa\" as if it were a value. Placeholder handling is covered in more depth in ",[51,719,721],{"href":720},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Freplace-placeholder-text-with-nan-in-pandas\u002F","Replace Placeholder Text with NaN in pandas",[57,723,725],{"id":724},"step-3-two-columns-combine_first","Step 3 — Two columns: combine_first",[10,727,728,729,732],{},"For exactly two columns, ",[14,730,731],{},"combine_first"," reads well: \"this, and where it is missing, that\":",[62,734,736],{"className":114,"code":735,"language":116,"meta":67,"style":67},"contacts[\"Phone (2 cols)\"] = phones[\"Mobile\"].combine_first(phones[\"Work phone\"])\nprint(contacts[[\"Name\", \"Phone (2 cols)\"]].to_string(index=False))\n",[14,737,738,762],{"__ignoreMap":67},[71,739,740,742,745,747,749,752,754,757,759],{"class":73,"line":74},[71,741,658],{"class":127},[71,743,744],{"class":81},"\"Phone (2 cols)\"",[71,746,664],{"class":127},[71,748,150],{"class":123},[71,750,751],{"class":127}," phones[",[71,753,195],{"class":81},[71,755,756],{"class":127},"].combine_first(phones[",[71,758,200],{"class":81},[71,760,761],{"class":127},"])\n",[71,763,764,766,768,770,772,774,776,778,780,782],{"class":73,"line":137},[71,765,253],{"class":167},[71,767,256],{"class":127},[71,769,259],{"class":81},[71,771,17],{"class":127},[71,773,744],{"class":81},[71,775,697],{"class":127},[71,777,272],{"class":161},[71,779,150],{"class":123},[71,781,178],{"class":167},[71,783,279],{"class":127},[10,785,786,788,789,793,794,796,797,800],{},[14,787,731],{}," also works between whole DataFrames aligned on the index — useful for filling gaps in one table from another with the same keys, the job described in ",[51,790,792],{"href":791},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Fupdate-one-excel-file-from-another-with-pandas\u002F","Update One Excel File from Another with pandas",". For three or more columns, chaining ",[14,795,731],{}," works but the ",[14,798,799],{},"bfill"," form is shorter and keeps the priority visible as a list.",[57,802,804],{"id":803},"step-4-record-which-column-supplied-the-value","Step 4 — Record which column supplied the value",[10,806,807,808,811],{},"People who maintain the source want to know ",[96,809,810],{},"where"," each number came from — a contact whose only number is a home phone may need follow-up. The position of the first non-missing value per row gives the source column:",[62,813,815],{"className":114,"code":814,"language":116,"meta":67,"style":67},"has_value = phones.notna()\ncontacts[\"Phone source\"] = has_value.idxmax(axis=1).where(has_value.any(axis=1), \"none\")\nprint(contacts[[\"Name\", \"Phone\", \"Phone source\"]].to_string(index=False))\nprint(contacts[\"Phone source\"].value_counts().to_dict())\n",[14,816,817,827,863,889],{"__ignoreMap":67},[71,818,819,822,824],{"class":73,"line":74},[71,820,821],{"class":127},"has_value ",[71,823,150],{"class":123},[71,825,826],{"class":127}," phones.notna()\n",[71,828,829,831,834,836,838,841,843,845,847,850,852,854,856,859,861],{"class":73,"line":137},[71,830,658],{"class":127},[71,832,833],{"class":81},"\"Phone source\"",[71,835,664],{"class":127},[71,837,150],{"class":123},[71,839,840],{"class":127}," has_value.idxmax(",[71,842,234],{"class":161},[71,844,150],{"class":123},[71,846,239],{"class":167},[71,848,849],{"class":127},").where(has_value.any(",[71,851,234],{"class":161},[71,853,150],{"class":123},[71,855,239],{"class":167},[71,857,858],{"class":127},"), ",[71,860,575],{"class":81},[71,862,181],{"class":127},[71,864,865,867,869,871,873,875,877,879,881,883,885,887],{"class":73,"line":144},[71,866,253],{"class":167},[71,868,256],{"class":127},[71,870,259],{"class":81},[71,872,17],{"class":127},[71,874,661],{"class":81},[71,876,17],{"class":127},[71,878,833],{"class":81},[71,880,697],{"class":127},[71,882,272],{"class":161},[71,884,150],{"class":123},[71,886,178],{"class":167},[71,888,279],{"class":127},[71,890,891,893,896,898],{"class":73,"line":184},[71,892,253],{"class":167},[71,894,895],{"class":127},"(contacts[",[71,897,833],{"class":81},[71,899,900],{"class":127},"].value_counts().to_dict())\n",[10,902,903,906,907,910,911,913,914,917],{},[14,904,905],{},"idxmax"," on a boolean frame returns the first column holding ",[14,908,909],{},"True",", which is exactly the column ",[14,912,799],{}," took the value from. Rows with no value at all would report the first column, so ",[14,915,916],{},"where(has_value.any(axis=1), \"none\")"," labels them explicitly. The counts summarise data quality at a glance: how many contacts are reachable by mobile, how many only by home phone, how many not at all.",[311,919,320,925,320,928,320,931,320,935,320,940,320,945,320,949,320,953,320,956,320,959,320,961,320,964,320,967,320,970,320,975,320,979,320,982,320,986,320,990,320,993],{"viewBox":920,"role":314,"ariaLabelledBy":921,"xmlns":318,"style":924},"4 0 744 206",[922,923],"co-sources-t","co-sources-d","width:100%;max-width:744px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[322,926,927],{"id":922},"Where each contact's phone number came from",[326,929,930],{"id":923},"Of six contacts, two phone numbers came from the Mobile column, two from Work phone, one from Home phone, and one contact has no number in any column.",[330,932],{"x":332,"y":245,"width":933,"height":934,"fill":335},"744","206",[330,936],{"x":338,"y":937,"width":938,"height":375,"rx":939,"fill":342,"stroke":343,"style":344},"16.0","109.3","7",[350,941,16],{"x":942,"y":943,"style":944},"74.6","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[330,946],{"x":947,"y":937,"width":948,"height":375,"rx":939,"fill":405,"stroke":406,"style":384},"139.3","576.8",[350,950,402],{"x":951,"y":943,"style":952},"724.1","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start",[330,954],{"x":338,"y":955,"width":938,"height":375,"rx":939,"fill":342,"stroke":343,"style":344},"56.0",[350,957,20],{"x":942,"y":958,"style":944},"74.5",[330,960],{"x":947,"y":955,"width":948,"height":375,"rx":939,"fill":382,"stroke":383,"style":384},[350,962,402],{"x":951,"y":958,"style":963},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start",[330,965],{"x":338,"y":966,"width":938,"height":375,"rx":939,"fill":342,"stroke":343,"style":344},"96.0",[350,968,24],{"x":942,"y":969,"style":944},"114.5",[330,971],{"x":947,"y":966,"width":972,"height":375,"rx":939,"fill":973,"stroke":974,"style":384},"288.4","#fdefd8","var(--gold,#b4740a)",[350,976,239],{"x":977,"y":969,"style":978},"435.7","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start",[330,980],{"x":338,"y":981,"width":938,"height":375,"rx":939,"fill":342,"stroke":343,"style":344},"136.0",[350,983,985],{"x":942,"y":984,"style":944},"154.5","none",[330,987],{"x":947,"y":981,"width":972,"height":375,"rx":939,"fill":988,"stroke":989,"style":384},"#fce9e9","var(--danger,#dc2626)",[350,991,239],{"x":977,"y":984,"style":992},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start",[350,994,998],{"x":995,"y":996,"style":997},"380.0","186.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","Ken's two placeholder cells were treated as missing, so he correctly shows no number.",[57,1000,1002],{"id":1001},"step-5-flag-rows-where-the-sources-disagree","Step 5 — Flag rows where the sources disagree",[10,1004,1005],{},"Coalescing hides conflicts: if the 2026 and 2025 price lists both have a value, the higher-priority one wins silently. For phone numbers several values are expected, but for facts that should agree — a customer's VAT number in two systems, a price in two lists — check before trusting the result:",[62,1007,1009],{"className":114,"code":1008,"language":116,"meta":67,"style":67},"def distinct_values(row):\n    return row.dropna().map(lambda v: v.replace(\" \", \"\")).nunique()\n\ncontacts[\"Distinct numbers\"] = phones.apply(distinct_values, axis=1)\nprint(contacts[[\"Name\", \"Distinct numbers\"]].to_string(index=False))\ndupes = contacts[(phones[\"Work phone\"].str.replace(\" \", \"\") == phones[\"Home phone\"].str.replace(\" \", \"\")).fillna(False)]\nprint(\"same number in work and home columns:\", dupes[\"Name\"].tolist())\n",[14,1010,1011,1021,1044,1048,1070,1092,1139],{"__ignoreMap":67},[71,1012,1013,1015,1018],{"class":73,"line":74},[71,1014,602],{"class":123},[71,1016,1017],{"class":605}," distinct_values",[71,1019,1020],{"class":127},"(row):\n",[71,1022,1023,1025,1028,1031,1034,1037,1039,1041],{"class":73,"line":137},[71,1024,630],{"class":123},[71,1026,1027],{"class":127}," row.dropna().map(",[71,1029,1030],{"class":123},"lambda",[71,1032,1033],{"class":127}," v: v.replace(",[71,1035,1036],{"class":81},"\" \"",[71,1038,17],{"class":127},[71,1040,222],{"class":81},[71,1042,1043],{"class":127},")).nunique()\n",[71,1045,1046],{"class":73,"line":144},[71,1047,141],{"emptyLinePlaceholder":140},[71,1049,1050,1052,1055,1057,1059,1062,1064,1066,1068],{"class":73,"line":184},[71,1051,658],{"class":127},[71,1053,1054],{"class":81},"\"Distinct numbers\"",[71,1056,664],{"class":127},[71,1058,150],{"class":123},[71,1060,1061],{"class":127}," phones.apply(distinct_values, ",[71,1063,234],{"class":161},[71,1065,150],{"class":123},[71,1067,239],{"class":167},[71,1069,181],{"class":127},[71,1071,1072,1074,1076,1078,1080,1082,1084,1086,1088,1090],{"class":73,"line":211},[71,1073,253],{"class":167},[71,1075,256],{"class":127},[71,1077,259],{"class":81},[71,1079,17],{"class":127},[71,1081,1054],{"class":81},[71,1083,697],{"class":127},[71,1085,272],{"class":161},[71,1087,150],{"class":123},[71,1089,178],{"class":167},[71,1091,279],{"class":127},[71,1093,1094,1097,1099,1102,1104,1107,1109,1111,1113,1116,1119,1121,1123,1125,1127,1129,1131,1134,1136],{"class":73,"line":250},[71,1095,1096],{"class":127},"dupes ",[71,1098,150],{"class":123},[71,1100,1101],{"class":127}," contacts[(phones[",[71,1103,200],{"class":81},[71,1105,1106],{"class":127},"].str.replace(",[71,1108,1036],{"class":81},[71,1110,17],{"class":127},[71,1112,222],{"class":81},[71,1114,1115],{"class":127},") ",[71,1117,1118],{"class":123},"==",[71,1120,751],{"class":127},[71,1122,205],{"class":81},[71,1124,1106],{"class":127},[71,1126,1036],{"class":81},[71,1128,17],{"class":127},[71,1130,222],{"class":81},[71,1132,1133],{"class":127},")).fillna(",[71,1135,178],{"class":167},[71,1137,1138],{"class":127},")]\n",[71,1140,1141,1143,1146,1149,1152,1154],{"class":73,"line":644},[71,1142,253],{"class":167},[71,1144,1145],{"class":127},"(",[71,1147,1148],{"class":81},"\"same number in work and home columns:\"",[71,1150,1151],{"class":127},", dupes[",[71,1153,259],{"class":81},[71,1155,1156],{"class":127},"].tolist())\n",[10,1158,1159,1160,55],{},"Counting distinct non-missing values per row (after removing spaces, so formatting differences do not count) shows how much information the coalesce threw away. Tomás has the same number in both work and home columns — harmless here, but in a price comparison two equal values are reassuring and two different ones are a problem to report. For value-by-value comparisons between files, see ",[51,1161,1163],{"href":1162},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcompare-two-excel-files-for-differences-with-python\u002F","Compare Two Excel Files for Differences with Python",[57,1165,1167],{"id":1166},"step-6-write-the-result-with-the-excel-formula-as-a-reference","Step 6 — Write the result, with the Excel formula as a reference",[62,1169,1171],{"className":114,"code":1170,"language":116,"meta":67,"style":67},"out = contacts[[\"ID\", \"Name\", \"Phone\", \"Phone source\"]].fillna({\"Phone\": \"\"})\nout.to_excel(\"contacts_clean.xlsx\", index=False)\nprint(out.to_string(index=False))\n",[14,1172,1173,1211,1229],{"__ignoreMap":67},[71,1174,1175,1178,1180,1183,1186,1188,1190,1192,1194,1196,1198,1201,1203,1206,1208],{"class":73,"line":74},[71,1176,1177],{"class":127},"out ",[71,1179,150],{"class":123},[71,1181,1182],{"class":127}," contacts[[",[71,1184,1185],{"class":81},"\"ID\"",[71,1187,17],{"class":127},[71,1189,259],{"class":81},[71,1191,17],{"class":127},[71,1193,661],{"class":81},[71,1195,17],{"class":127},[71,1197,833],{"class":81},[71,1199,1200],{"class":127},"]].fillna({",[71,1202,661],{"class":81},[71,1204,1205],{"class":127},": ",[71,1207,222],{"class":81},[71,1209,1210],{"class":127},"})\n",[71,1212,1213,1216,1219,1221,1223,1225,1227],{"class":73,"line":137},[71,1214,1215],{"class":127},"out.to_excel(",[71,1217,1218],{"class":81},"\"contacts_clean.xlsx\"",[71,1220,17],{"class":127},[71,1222,272],{"class":161},[71,1224,150],{"class":123},[71,1226,178],{"class":167},[71,1228,181],{"class":127},[71,1230,1231,1233,1236,1238,1240,1242],{"class":73,"line":144},[71,1232,253],{"class":167},[71,1234,1235],{"class":127},"(out.to_string(",[71,1237,272],{"class":161},[71,1239,150],{"class":123},[71,1241,178],{"class":167},[71,1243,279],{"class":127},[10,1245,1246,1247,1250,1251,1253,1254,1257],{},"If the people using the workbook prefer to keep the logic in Excel, the equivalent formula for row 2 is ",[14,1248,1249],{},"=IF(TRIM(C2)\u003C>\"\",C2,IF(TRIM(D2)\u003C>\"\",D2,IF(TRIM(E2)\u003C>\"\",E2,\"\")))"," — readable for three columns, unwieldy for six, and it still treats ",[14,1252,102],{}," as a phone number unless every test grows a placeholder check. Excel 365 users can write ",[14,1255,1256],{},"=IFERROR(INDEX(FILTER(C2:E2, TRIM(C2:E2)\u003C>\"\"), 1), \"\")"," to take the first non-empty value of the row. The pandas version keeps the placeholder list, the priority order and the source tracking in one place, which is the main reason to move this step into the script.",[311,1259,320,1264,320,1267,320,1270,320,1273,320,1278,320,1288,320,1294,320,1298,320,1304,320,1308,320,1313,320,1316,320,1319,320,1326,320,1332,320,1336,320,1339,320,1343,320,1345,320,1349,320,1352,320,1355,320,1362,320,1367,320,1369,320,1373,320,1375,320,1379,320,1382,320,1385,320,1392,320,1397,320,1400,320,1403,320,1407],{"viewBox":1260,"role":314,"ariaLabelledBy":1261,"xmlns":318,"style":319},"4 0 752 150",[1262,1263],"co-steps-t","co-steps-d",[322,1265,1266],{"id":1262},"A reliable coalesce in four steps",[326,1268,1269],{"id":1263},"Read the columns as text, turn blanks, spaces and placeholder words into missing values, take the first non-missing value in priority order with bfill, and record the source column and any conflicts.",[330,1271],{"x":332,"y":245,"width":333,"height":1272,"fill":335},"150",[330,1274],{"x":338,"y":937,"width":1275,"height":1276,"rx":1277,"fill":342,"stroke":343,"style":511},"153.0","118.5","12",[1279,1280,1282],"g",{"transform":1281},"translate(84.5,41.8) scale(1.000)",[1283,1284],"path",{"d":1285,"fill":985,"stroke":1286,"style":1287},"M6 3h9l4 4v14H6z M15 3v4h4","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[350,1289,1293],{"x":1290,"y":1291,"style":1292},"96.5","88.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Read as text",[350,1295,296],{"x":1290,"y":1296,"style":1297},"107.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[1299,1300],"circle",{"cx":378,"cy":340,"r":1301,"fill":1302,"stroke":985,"style":1303},"11.0","#5b6780","stroke-width:0px",[350,1305,239],{"x":378,"y":1306,"style":1307},"36.5","font-size:12px;font-weight:700;fill:#ffffff;text-anchor:middle",[73,1309],{"x1":1310,"y1":1311,"x2":1312,"y2":1311,"stroke":383,"style":511},"178.0","75.2","200.0",[513,1314],{"points":1315,"fill":516},"204.0,75.2 195.0,80.2 195.0,70.2",[330,1317],{"x":1318,"y":937,"width":1275,"height":1276,"rx":1277,"fill":973,"stroke":974,"style":511},"209.0",[1279,1320,1322],{"transform":1321},"translate(273.5,33.0) scale(1.000)",[1283,1323],{"d":1324,"fill":985,"stroke":1325,"style":1287},"M3 5h18l-7 8v6l-4 2v-8z","var(--gold-ink,#7a4e06)",[350,1327,1331],{"x":1328,"y":1329,"style":1330},"285.5","79.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Normalise missing",[350,1333,1335],{"x":1328,"y":1334,"style":1297},"99.0","strip, placeholders →",[350,1337,228],{"x":1328,"y":1338,"style":1297},"116.5",[1299,1340],{"cx":1341,"cy":340,"r":1301,"fill":1342,"stroke":985,"style":1303},"225.0","#8a5808",[350,1344,402],{"x":1341,"y":1306,"style":1307},[73,1346],{"x1":1347,"y1":1311,"x2":1348,"y2":1311,"stroke":383,"style":511},"367.0","389.0",[513,1350],{"points":1351,"fill":516},"393.0,75.2 384.0,80.2 384.0,70.2",[330,1353],{"x":1354,"y":937,"width":1275,"height":1276,"rx":1277,"fill":382,"stroke":383,"style":511},"398.0",[1279,1356,1358],{"transform":1357},"translate(462.5,41.8) scale(1.000)",[1283,1359],{"d":1360,"fill":985,"stroke":1361,"style":1287},"M6 4v5a5 5 0 0 0 5 5h7 M6 20v-5 M15 11l3 3-3 3","var(--brand-strong,#4338ca)",[350,1363,1366],{"x":1364,"y":1291,"style":1365},"474.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Coalesce",[350,1368,530],{"x":1364,"y":1296,"style":1297},[1299,1370],{"cx":1371,"cy":340,"r":1301,"fill":1372,"stroke":985,"style":1303},"414.0","#4338ca",[350,1374,425],{"x":1371,"y":1306,"style":1307},[73,1376],{"x1":1377,"y1":1311,"x2":1378,"y2":1311,"stroke":383,"style":511},"556.0","578.0",[513,1380],{"points":1381,"fill":516},"582.0,75.2 573.0,80.2 573.0,70.2",[330,1383],{"x":1384,"y":937,"width":1275,"height":1276,"rx":1277,"fill":405,"stroke":406,"style":511},"587.0",[1279,1386,1388],{"transform":1387},"translate(651.5,33.0) scale(1.000)",[1283,1389],{"d":1390,"fill":985,"stroke":1391,"style":1287},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[350,1393,1396],{"x":1394,"y":1329,"style":1395},"663.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Record source",[350,1398,1399],{"x":1394,"y":1334,"style":1297},"idxmax + conflict",[350,1401,1402],{"x":1394,"y":1338,"style":1297},"count",[1299,1404],{"cx":1405,"cy":340,"r":1301,"fill":1406,"stroke":985,"style":1303},"603.0","#0b6157",[350,1408,332],{"x":1405,"y":1306,"style":1307},[57,1410,1412],{"id":1411},"performance-notes","Performance notes",[10,1414,1415,1417,1418,1420,1421,1424,1425,1428,1429,1432,1433,1436],{},[14,1416,284],{}," and the boolean ",[14,1419,905],{}," are vectorised and handle hundreds of thousands of rows in a fraction of a second. The slow pieces are the row-wise ",[14,1422,1423],{},"apply"," calls: the placeholder clean-up in Step 2 applies a vectorised function once per ",[96,1426,1427],{},"column",", which is cheap, but the distinct-value count in Step 5 runs Python code once per ",[96,1430,1431],{},"row",". For large tables, compute conflicts with column comparisons instead — for two or three columns, ",[14,1434,1435],{},"(a != b) & a.notna() & b.notna()"," per pair is fast — and keep the per-row version for small review extracts.",[10,1438,1439,1440,1443,1444,1447],{},"The same coalesce logic often needs to run across many files with slightly different column names. Keep the priority list as configuration — ",[14,1441,1442],{},"{\"Phone\": [\"Mobile\", \"Cell\", \"Work phone\", \"Home phone\"]}"," — and use only the columns that exist in each file (",[14,1445,1446],{},"[c for c in order if c in df.columns]","), so a sheet without a \"Cell\" column does not break the job.",[57,1449,1451],{"id":1450},"common-pitfalls","Common pitfalls",[1453,1454,1455,1470,1482,1488,1494,1500],"ul",{},[1456,1457,1458,1462,1463,17,1465,21,1467,1469],"li",{},[1459,1460,1461],"strong",{},"Treating placeholders as values."," ",[14,1464,1036],{},[14,1466,560],{},[14,1468,565],{}," are not phone numbers; normalise before coalescing.",[1456,1471,1472,1475,1476,1462,1478,1481],{},[1459,1473,1474],{},"Wrong priority."," The column order passed to ",[14,1477,799],{},[96,1479,1480],{},"is"," the rule; write it down as a named list.",[1456,1483,1484,1487],{},[1459,1485,1486],{},"Numbers read as numbers."," Phone numbers and IDs lose leading zeros; read them as text.",[1456,1489,1490,1493],{},[1459,1491,1492],{},"Silent conflicts."," Coalescing picks one value without telling you the others differed; count distinct values when the sources should agree.",[1456,1495,1496,1499],{},[1459,1497,1498],{},"Mixed dtypes across columns."," Coalescing a text column with a numeric one gives an object column; convert to one type first.",[1456,1501,1502,1462,1505,21,1507,1510],{},[1459,1503,1504],{},"Formatting differences treated as different values.",[14,1506,432],{},[14,1508,1509],{},"01134960002"," are the same number; normalise formatting before comparing sources.",[57,1512,1514],{"id":1513},"conclusion","Conclusion",[10,1516,1517,1518,1521,1522,1525,1526,1528],{},"To take the first non-empty value across several columns, first turn blanks, whitespace and placeholder text into real missing values, then use ",[14,1519,1520],{},"df[cols].bfill(axis=1).iloc[:, 0]"," with the columns in priority order — or ",[14,1523,1524],{},"a.combine_first(b)"," for two columns. Record the source column with ",[14,1527,905],{}," on the not-null mask, count distinct values to expose conflicts, and write the result back with the source alongside, so the people who own the data can see which records rely on a fallback.",[57,1530,1532],{"id":1531},"frequently-asked-questions","Frequently asked questions",[10,1534,1535,1538,1539,1542,1543,1546],{},[1459,1536,1537],{},"How do I get the first non-empty value from several columns in pandas?","\nSelect the columns in priority order and use df",[71,1540,1541],{},"cols",".bfill(axis=1).iloc",[71,1544,1545],{},":, 0",". Make sure empty strings and placeholders such as \"n\u002Fa\" are converted to NA first, or they count as values.",[10,1548,1549,1552,1553,1555],{},[1459,1550,1551],{},"What is the pandas equivalent of SQL COALESCE?","\nFor two columns, a.combine_first(b). For any number of columns, df[[c1, c2, c3]].bfill(axis=1).iloc",[71,1554,1545],{},", which returns the first non-missing value in the given order.",[10,1557,1558,1561,1562,1564],{},[1459,1559,1560],{},"How can I tell which column the coalesced value came from?","\nBuild a boolean frame of non-missing values with df",[71,1563,1541],{},".notna() and call idxmax(axis=1); it returns the first column that has a value. Label rows with no value separately, because idxmax returns the first column for them.",[10,1566,1567,1570],{},[1459,1568,1569],{},"Why does my coalesced column still contain blanks?","\nThe cells contain spaces or placeholder text rather than being truly empty, so pandas treats them as values. Strip the text and replace placeholders with NA before coalescing.",[57,1572,1574],{"id":1573},"related","Related",[1453,1576,1577,1583,1590,1597],{},[1456,1578,1579,1580,1582],{},"Up one level: ",[51,1581,54],{"href":53}," — tidying messy spreadsheet data.",[1456,1584,1585,1589],{},[51,1586,1588],{"href":1587},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Ffill-missing-values-in-excel-with-pandas-fillna\u002F","Fill Missing Values in Excel with pandas fillna"," — other ways to fill gaps.",[1456,1591,1592,1596],{},[51,1593,1595],{"href":1594},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Ftell-blank-cells-empty-strings-and-zeros-apart-in-excel-data\u002F","Tell Blank Cells, Empty Strings and Zeros Apart in Excel Data"," — what \"empty\" really means in a sheet.",[1456,1598,1599,1603],{},[51,1600,1602],{"href":1601},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fiferror-and-isna-equivalents-in-pandas\u002F","IFERROR and ISNA Equivalents in pandas"," — fallbacks for errors rather than blanks.",[1605,1606,1607],"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 .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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":67,"searchDepth":137,"depth":137,"links":1609},[1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621],{"id":59,"depth":137,"text":60},{"id":110,"depth":137,"text":111},{"id":533,"depth":137,"text":534},{"id":724,"depth":137,"text":725},{"id":803,"depth":137,"text":804},{"id":1001,"depth":137,"text":1002},{"id":1166,"depth":137,"text":1167},{"id":1411,"depth":137,"text":1412},{"id":1450,"depth":137,"text":1451},{"id":1513,"depth":137,"text":1514},{"id":1531,"depth":137,"text":1532},{"id":1573,"depth":137,"text":1574},"2026-10-02","Take the first non-empty value from several columns — Mobile, else Work phone, else Home phone — the way SQL's COALESCE or a chain of nested IFs in Excel does: bfill across columns, combine_first for two columns, treating blanks and placeholder text as missing, recording which column supplied each value, and flagging rows where the sources disagree.","md",[1626,1628,1630,1632],{"q":1537,"a":1627},"Select the columns in priority order and use df[cols].bfill(axis=1).iloc[:, 0]. Make sure empty strings and placeholders such as \"n\u002Fa\" are converted to NA first, or they count as values.",{"q":1551,"a":1629},"For two columns, a.combine_first(b). For any number of columns, df[[c1, c2, c3]].bfill(axis=1).iloc[:, 0], which returns the first non-missing value in the given order.",{"q":1560,"a":1631},"Build a boolean frame of non-missing values with df[cols].notna() and call idxmax(axis=1); it returns the first column that has a value. Label rows with no value separately, because idxmax returns the first column for them.",{"q":1569,"a":1633},"The cells contain spaces or placeholder text rather than being truly empty, so pandas treats them as values. Strip the text and replace placeholders with NA before coalescing.","coalesce several excel columns into one with pandas",null,{"breadcrumb":1637},[1638,1641,1644],{"name":1639,"item":1640},"Home","\u002F",{"name":1642,"item":1643},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":1645,"item":53},"Cleaning Excel Data With Pandas: A Production Workflow","\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fcoalesce-several-excel-columns-into-one-with-pandas",{"title":1648,"description":1649},"Coalesce Multiple Excel Columns into One with pandas","First non-empty value across columns in pandas: bfill(axis=1).iloc[:, 0], combine_first, blanks and \"n\u002Fa\" as missing, a Source column from idxmax, conflicting values check, and the equivalent nested IF in Excel.","coalesce-several-excel-columns-into-one-with-pandas","advanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fcoalesce-several-excel-columns-into-one-with-pandas\u002Findex",[1653,1654,1655],"data-cleaning","missing-data","pandas","how-to","EaPKUqdWfDCOLYBbd2s5bYFg7hd7AVtKFWvbOWFWx_4",[1659,1663],{"title":1660,"path":1661,"stem":1662,"children":-1},"Clean Mixed-Currency Amounts into Amount and Currency Columns","\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fclean-mixed-currency-amounts-into-amount-and-currency-columns","advanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fclean-mixed-currency-amounts-into-amount-and-currency-columns\u002Findex",{"title":1664,"path":1665,"stem":1666,"children":-1},"Convert Currency Strings Like \"$1,234.56\" and \"(500)\" to Numbers with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-currency-strings-to-numbers-with-pandas","advanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-currency-strings-to-numbers-with-pandas\u002Findex",[1668,1670],{"path":1669,"title":1645},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas",{"path":1671,"title":1642},"\u002Fadvanced-data-transformation-and-cleaning",[],[1674,1677,1680,1681,1682,1685],{"path":1675,"title":1676,"kind":1656},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Ffind-duplicate-rows-in-excel-with-python","Find Duplicate Rows in Excel with Python",{"path":1678,"title":1679,"kind":1656},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-duplicate-values-in-excel-with-openpyxl","Highlight Duplicate Values in Excel with openpyxl",{"path":1661,"title":1660,"kind":1656},{"path":1665,"title":1664,"kind":1656},{"path":1683,"title":1684,"kind":1656},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-excel-text-columns-to-numbers-with-pandas","Convert Excel Text Columns to Numbers with pandas",{"path":1686,"title":1687,"kind":1656},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-percentage-strings-to-numbers-with-pandas","Convert Percentage Strings Like \"12.5%\" to Numbers with pandas",1790972080531]