[{"data":1,"prerenderedAt":2430},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fexcel-text-functions-left-right-mid-and-concat-in-pandas":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fexcel-text-functions-left-right-mid-and-concat-in-pandas":2421},{"id":4,"title":5,"body":6,"dateModified":2392,"datePublished":2392,"description":2393,"extension":2394,"faq":2395,"meta":2405,"navigation":228,"path":2414,"seo":2415,"slug":2417,"stem":2418,"type":2419,"__hash__":2420},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fexcel-text-functions-left-right-mid-and-concat-in-pandas\u002Findex.md","Excel Text Functions LEFT, RIGHT, MID and CONCAT in pandas",{"type":7,"value":8,"toc":2377},"minimark",[9,19,166,171,200,402,417,421,562,569,576,657,660,664,852,872,876,975,1123,1137,1155,1159,1377,1383,1387,1390,1530,1540,1544,1551,1751,1757,1764,1768,1771,1942,1948,1952,2074,2078,2173,2187,2252,2259,2263,2286,2290,2313,2319,2325,2331,2335,2373],[10,11,12,13,18],"p",{},"Excel's text functions exist because a formula operates on one value at a time. pandas operates on\nwhole columns, so LEFT, RIGHT and MID collapse into ordinary slicing and CONCATENATE becomes an\noperator — and the string methods that have no spreadsheet equivalent, like splitting a column into\nseveral or applying a regular expression, come along for free. This guide is part of\n",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002F","Excel Formula Equivalents in pandas",".",[20,21,29,30,29,34,29,38,29,45,29,54,29,61,29,65,29,69,29,74,29,79,29,83,29,86,29,91,29,96,29,99,29,102,29,105,29,109,29,112,29,115,29,120,29,125,29,128,29,131,29,134,29,138,29,141,29,144,29,149,29,154,29,157,29,160],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"0 0 760 336","img",[25,26],"txt-map-t","txt-map-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[31,32,33],"title",{"id":25},"Excel text functions and their column-wide counterparts",[35,36,37],"desc",{"id":26},"LEFT, RIGHT and MID all become slice syntax on the string accessor, CONCATENATE becomes the plus operator, TEXTJOIN becomes str.cat with a placeholder, and TRIM needs both strip and a whitespace collapse.",[39,40],"rect",{"x":41,"y":41,"width":42,"height":43,"fill":44},"0","760","336","#ffffff",[39,46],{"x":47,"y":48,"width":49,"height":50,"rx":51,"fill":52,"stroke":53},"18","30","724","34","8","#e7ebef","var(--line,#cdd5e6)",[55,56,60],"text",{"x":57,"y":58,"style":59},"126.6","52","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","Excel",[55,62,64],{"x":63,"y":58,"style":59},"361.9","pandas",[55,66,68],{"x":67,"y":58,"style":59},"615.3000000000001","Note",[39,70],{"x":47,"y":71,"width":49,"height":50,"rx":51,"fill":72,"stroke":73},"67","#d9f4f1","var(--teal,#0f9488)",[55,75,78],{"x":57,"y":76,"style":77},"88.0","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","LEFT \u002F RIGHT \u002F MID",[55,80,82],{"x":63,"y":76,"style":81},"font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",".str[a:b]",[55,84,85],{"x":67,"y":76,"style":81},"one syntax, three functions",[39,87],{"x":47,"y":88,"width":49,"height":50,"rx":51,"fill":89,"stroke":90},"107","#f0f4ff","var(--brand,#5b5cf0)",[55,92,95],{"x":57,"y":93,"style":94},"128.0","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","CONCATENATE \u002F &",[55,97,98],{"x":63,"y":93,"style":81},"+",[55,100,101],{"x":67,"y":93,"style":81},"missing values propagate",[39,103],{"x":47,"y":104,"width":49,"height":50,"rx":51,"fill":89,"stroke":90},"147",[55,106,108],{"x":57,"y":107,"style":94},"168.0","TEXTJOIN",[55,110,111],{"x":63,"y":107,"style":81},"str.cat(na_rep=...)",[55,113,114],{"x":67,"y":107,"style":81},"placeholder for blanks",[39,116],{"x":47,"y":117,"width":49,"height":50,"rx":51,"fill":118,"stroke":119},"187","#fdefd8","var(--gold,#b4740a)",[55,121,124],{"x":57,"y":122,"style":123},"208.0","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","TRIM",[55,126,127],{"x":63,"y":122,"style":81},"strip + collapse runs",[55,129,130],{"x":67,"y":122,"style":81},"two steps to match",[39,132],{"x":47,"y":133,"width":49,"height":50,"rx":51,"fill":118,"stroke":119},"227",[55,135,137],{"x":57,"y":136,"style":123},"248.0","SUBSTITUTE",[55,139,140],{"x":63,"y":136,"style":81},"str.replace(regex=False)",[55,142,143],{"x":67,"y":136,"style":81},"literal by default in Excel",[39,145],{"x":47,"y":146,"width":49,"height":50,"rx":51,"fill":147,"stroke":148},"267","#fee8f2","var(--accent,#d81b73)",[55,150,153],{"x":57,"y":151,"style":152},"288.0","font-size:12px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:middle","no equivalent",[55,155,156],{"x":63,"y":151,"style":81},"str.extract(pattern)",[55,158,159],{"x":67,"y":151,"style":81},"replaces nested FIND and MID",[55,161,165],{"x":162,"y":163,"style":164},"380.0","324","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","the last row is where most formula disappears",[167,168,170],"h2",{"id":169},"prerequisites","Prerequisites",[172,173,178],"pre",{"className":174,"code":175,"language":176,"meta":177,"style":177},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[179,180,181],"code",{"__ignoreMap":177},[182,183,186,190,194,197],"span",{"class":184,"line":185},"line",1,[182,187,189],{"class":188},"sMTad","pip",[182,191,193],{"class":192},"srMev"," install",[182,195,196],{"class":192}," pandas",[182,198,199],{"class":192}," openpyxl\n",[172,201,205],{"className":202,"code":203,"language":204,"meta":177,"style":177},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nrecords = pd.DataFrame({\n    \"Reference\": [\"INV-2026-000412\", \"INV-2026-000413\", \"CRN-2026-000044\", \"INV-2025-009871\"],\n    \"Customer\": [\"  Acme Ltd \", \"Brightline PLC\", \"cobalt systems\", None],\n    \"Contact\": [\"ana@acme.example\", \"ben@brightline.example\", None, \"dev@cobalt.example\"],\n    \"Postcode\": [\"SW1A 1AA\", \"M1 4BT\", \"EH8 9YL\", \"BS1 5TR\"],\n}).astype({\"Reference\": \"string\", \"Customer\": \"string\", \"Contact\": \"string\", \"Postcode\": \"string\"})\n","python",[179,206,207,223,230,242,273,302,329,357],{"__ignoreMap":177},[182,208,209,213,217,220],{"class":184,"line":185},[182,210,212],{"class":211},"s-kum","import",[182,214,216],{"class":215},"skGVy"," pandas ",[182,218,219],{"class":211},"as",[182,221,222],{"class":215}," pd\n",[182,224,226],{"class":184,"line":225},2,[182,227,229],{"emptyLinePlaceholder":228},true,"\n",[182,231,233,236,239],{"class":184,"line":232},3,[182,234,235],{"class":215},"records ",[182,237,238],{"class":211},"=",[182,240,241],{"class":215}," pd.DataFrame({\n",[182,243,245,248,251,254,257,260,262,265,267,270],{"class":184,"line":244},4,[182,246,247],{"class":192},"    \"Reference\"",[182,249,250],{"class":215},": [",[182,252,253],{"class":192},"\"INV-2026-000412\"",[182,255,256],{"class":215},", ",[182,258,259],{"class":192},"\"INV-2026-000413\"",[182,261,256],{"class":215},[182,263,264],{"class":192},"\"CRN-2026-000044\"",[182,266,256],{"class":215},[182,268,269],{"class":192},"\"INV-2025-009871\"",[182,271,272],{"class":215},"],\n",[182,274,276,279,281,284,286,289,291,294,296,300],{"class":184,"line":275},5,[182,277,278],{"class":192},"    \"Customer\"",[182,280,250],{"class":215},[182,282,283],{"class":192},"\"  Acme Ltd \"",[182,285,256],{"class":215},[182,287,288],{"class":192},"\"Brightline PLC\"",[182,290,256],{"class":215},[182,292,293],{"class":192},"\"cobalt systems\"",[182,295,256],{"class":215},[182,297,299],{"class":298},"sP0c6","None",[182,301,272],{"class":215},[182,303,305,308,310,313,315,318,320,322,324,327],{"class":184,"line":304},6,[182,306,307],{"class":192},"    \"Contact\"",[182,309,250],{"class":215},[182,311,312],{"class":192},"\"ana@acme.example\"",[182,314,256],{"class":215},[182,316,317],{"class":192},"\"ben@brightline.example\"",[182,319,256],{"class":215},[182,321,299],{"class":298},[182,323,256],{"class":215},[182,325,326],{"class":192},"\"dev@cobalt.example\"",[182,328,272],{"class":215},[182,330,332,335,337,340,342,345,347,350,352,355],{"class":184,"line":331},7,[182,333,334],{"class":192},"    \"Postcode\"",[182,336,250],{"class":215},[182,338,339],{"class":192},"\"SW1A 1AA\"",[182,341,256],{"class":215},[182,343,344],{"class":192},"\"M1 4BT\"",[182,346,256],{"class":215},[182,348,349],{"class":192},"\"EH8 9YL\"",[182,351,256],{"class":215},[182,353,354],{"class":192},"\"BS1 5TR\"",[182,356,272],{"class":215},[182,358,360,363,366,369,372,374,377,379,381,383,386,388,390,392,395,397,399],{"class":184,"line":359},8,[182,361,362],{"class":215},"}).astype({",[182,364,365],{"class":192},"\"Reference\"",[182,367,368],{"class":215},": ",[182,370,371],{"class":192},"\"string\"",[182,373,256],{"class":215},[182,375,376],{"class":192},"\"Customer\"",[182,378,368],{"class":215},[182,380,371],{"class":192},[182,382,256],{"class":215},[182,384,385],{"class":192},"\"Contact\"",[182,387,368],{"class":215},[182,389,371],{"class":192},[182,391,256],{"class":215},[182,393,394],{"class":192},"\"Postcode\"",[182,396,368],{"class":215},[182,398,371],{"class":192},[182,400,401],{"class":215},"})\n",[10,403,404,405,408,409,412,413,416],{},"Casting to the nullable ",[179,406,407],{},"string"," dtype is worth doing up front: it keeps missing values as ",[179,410,411],{},"pd.NA","\nrather than ",[179,414,415],{},"float('nan')",", which makes the string methods behave consistently.",[167,418,420],{"id":419},"left-right-and-mid-become-slicing","LEFT, RIGHT and MID become slicing",[172,422,424],{"className":202,"code":423,"language":204,"meta":177,"style":177},"# =LEFT(A2, 3)\nrecords[\"Doc_Type\"] = records[\"Reference\"].str[:3]\n\n# =RIGHT(A2, 6)\nrecords[\"Serial\"] = records[\"Reference\"].str[-6:]\n\n# =MID(A2, 5, 4)\nrecords[\"Year\"] = records[\"Reference\"].str[4:8]\n\nprint(records[[\"Reference\", \"Doc_Type\", \"Year\", \"Serial\"]])\n",[179,425,426,432,459,463,468,495,499,504,531,536],{"__ignoreMap":177},[182,427,428],{"class":184,"line":185},[182,429,431],{"class":430},"s-wDw","# =LEFT(A2, 3)\n",[182,433,434,437,440,443,445,448,450,453,456],{"class":184,"line":225},[182,435,436],{"class":215},"records[",[182,438,439],{"class":192},"\"Doc_Type\"",[182,441,442],{"class":215},"] ",[182,444,238],{"class":211},[182,446,447],{"class":215}," records[",[182,449,365],{"class":192},[182,451,452],{"class":215},"].str[:",[182,454,455],{"class":298},"3",[182,457,458],{"class":215},"]\n",[182,460,461],{"class":184,"line":232},[182,462,229],{"emptyLinePlaceholder":228},[182,464,465],{"class":184,"line":244},[182,466,467],{"class":430},"# =RIGHT(A2, 6)\n",[182,469,470,472,475,477,479,481,483,486,489,492],{"class":184,"line":275},[182,471,436],{"class":215},[182,473,474],{"class":192},"\"Serial\"",[182,476,442],{"class":215},[182,478,238],{"class":211},[182,480,447],{"class":215},[182,482,365],{"class":192},[182,484,485],{"class":215},"].str[",[182,487,488],{"class":211},"-",[182,490,491],{"class":298},"6",[182,493,494],{"class":215},":]\n",[182,496,497],{"class":184,"line":304},[182,498,229],{"emptyLinePlaceholder":228},[182,500,501],{"class":184,"line":331},[182,502,503],{"class":430},"# =MID(A2, 5, 4)\n",[182,505,506,508,511,513,515,517,519,521,524,527,529],{"class":184,"line":359},[182,507,436],{"class":215},[182,509,510],{"class":192},"\"Year\"",[182,512,442],{"class":215},[182,514,238],{"class":211},[182,516,447],{"class":215},[182,518,365],{"class":192},[182,520,485],{"class":215},[182,522,523],{"class":298},"4",[182,525,526],{"class":215},":",[182,528,51],{"class":298},[182,530,458],{"class":215},[182,532,534],{"class":184,"line":533},9,[182,535,229],{"emptyLinePlaceholder":228},[182,537,539,542,545,547,549,551,553,555,557,559],{"class":184,"line":538},10,[182,540,541],{"class":298},"print",[182,543,544],{"class":215},"(records[[",[182,546,365],{"class":192},[182,548,256],{"class":215},[182,550,439],{"class":192},[182,552,256],{"class":215},[182,554,510],{"class":192},[182,556,256],{"class":215},[182,558,474],{"class":192},[182,560,561],{"class":215},"]])\n",[10,563,564,565,568],{},"One syntax replaces three functions, and it composes: ",[179,566,567],{},"records[\"Reference\"].str[4:8].astype(int)","\nturns the extracted year into a number in the same expression. The slice is also forgiving in a way\nMID is not — asking for more characters than the string has returns what exists rather than an error.",[10,570,571,572,575],{},"Where the position is not fixed, ",[179,573,574],{},"str.split"," is more robust than counting characters:",[172,577,579],{"className":202,"code":578,"language":204,"meta":177,"style":177},"parts = records[\"Reference\"].str.split(\"-\", expand=True)\nparts.columns = [\"Doc_Type\", \"Year\", \"Serial\"]\nrecords[[\"Doc_Type\", \"Year\", \"Serial\"]] = parts\n",[179,580,581,612,634],{"__ignoreMap":177},[182,582,583,586,588,590,592,595,598,600,604,606,609],{"class":184,"line":185},[182,584,585],{"class":215},"parts ",[182,587,238],{"class":211},[182,589,447],{"class":215},[182,591,365],{"class":192},[182,593,594],{"class":215},"].str.split(",[182,596,597],{"class":192},"\"-\"",[182,599,256],{"class":215},[182,601,603],{"class":602},"sa561","expand",[182,605,238],{"class":211},[182,607,608],{"class":298},"True",[182,610,611],{"class":215},")\n",[182,613,614,617,619,622,624,626,628,630,632],{"class":184,"line":225},[182,615,616],{"class":215},"parts.columns ",[182,618,238],{"class":211},[182,620,621],{"class":215}," [",[182,623,439],{"class":192},[182,625,256],{"class":215},[182,627,510],{"class":192},[182,629,256],{"class":215},[182,631,474],{"class":192},[182,633,458],{"class":215},[182,635,636,639,641,643,645,647,649,652,654],{"class":184,"line":232},[182,637,638],{"class":215},"records[[",[182,640,439],{"class":192},[182,642,256],{"class":215},[182,644,510],{"class":192},[182,646,256],{"class":215},[182,648,474],{"class":192},[182,650,651],{"class":215},"]] ",[182,653,238],{"class":211},[182,655,656],{"class":215}," parts\n",[10,658,659],{},"That is Text to Columns in one line, and unlike the fixed-width version it survives a reference\nformat whose serial number grows a digit.",[167,661,663],{"id":662},"concatenate-textjoin-and-the-ampersand","CONCATENATE, TEXTJOIN and the ampersand",[172,665,667],{"className":202,"code":666,"language":204,"meta":177,"style":177},"# =A2 & \" \u002F \" & D2\nrecords[\"Label\"] = records[\"Reference\"] + \" \u002F \" + records[\"Postcode\"]\n\n# =TEXTJOIN(\" | \", TRUE, A2, B2, D2) — skipping blanks\nrecords[\"Summary\"] = records[[\"Reference\", \"Customer\", \"Postcode\"]].apply(\n    lambda row: \" | \".join(part for part in row if pd.notna(part)), axis=1\n)\n\n# str.cat with a placeholder rather than dropping the row's result\nrecords[\"Joined\"] = records[\"Reference\"].str.cat(records[\"Customer\"], sep=\" — \", na_rep=\"(no name)\")\nprint(records[[\"Label\", \"Joined\"]])\n",[179,668,669,674,705,709,714,741,781,785,789,794,837],{"__ignoreMap":177},[182,670,671],{"class":184,"line":185},[182,672,673],{"class":430},"# =A2 & \" \u002F \" & D2\n",[182,675,676,678,681,683,685,687,689,691,693,696,699,701,703],{"class":184,"line":225},[182,677,436],{"class":215},[182,679,680],{"class":192},"\"Label\"",[182,682,442],{"class":215},[182,684,238],{"class":211},[182,686,447],{"class":215},[182,688,365],{"class":192},[182,690,442],{"class":215},[182,692,98],{"class":211},[182,694,695],{"class":192}," \" \u002F \"",[182,697,698],{"class":211}," +",[182,700,447],{"class":215},[182,702,394],{"class":192},[182,704,458],{"class":215},[182,706,707],{"class":184,"line":232},[182,708,229],{"emptyLinePlaceholder":228},[182,710,711],{"class":184,"line":244},[182,712,713],{"class":430},"# =TEXTJOIN(\" | \", TRUE, A2, B2, D2) — skipping blanks\n",[182,715,716,718,721,723,725,728,730,732,734,736,738],{"class":184,"line":275},[182,717,436],{"class":215},[182,719,720],{"class":192},"\"Summary\"",[182,722,442],{"class":215},[182,724,238],{"class":211},[182,726,727],{"class":215}," records[[",[182,729,365],{"class":192},[182,731,256],{"class":215},[182,733,376],{"class":192},[182,735,256],{"class":215},[182,737,394],{"class":192},[182,739,740],{"class":215},"]].apply(\n",[182,742,743,746,749,752,755,758,761,764,767,770,773,776,778],{"class":184,"line":304},[182,744,745],{"class":211},"    lambda",[182,747,748],{"class":215}," row: ",[182,750,751],{"class":192},"\" | \"",[182,753,754],{"class":215},".join(part ",[182,756,757],{"class":211},"for",[182,759,760],{"class":215}," part ",[182,762,763],{"class":211},"in",[182,765,766],{"class":215}," row ",[182,768,769],{"class":211},"if",[182,771,772],{"class":215}," pd.notna(part)), ",[182,774,775],{"class":602},"axis",[182,777,238],{"class":211},[182,779,780],{"class":298},"1\n",[182,782,783],{"class":184,"line":331},[182,784,611],{"class":215},[182,786,787],{"class":184,"line":359},[182,788,229],{"emptyLinePlaceholder":228},[182,790,791],{"class":184,"line":533},[182,792,793],{"class":430},"# str.cat with a placeholder rather than dropping the row's result\n",[182,795,796,798,801,803,805,807,809,812,814,817,820,822,825,827,830,832,835],{"class":184,"line":538},[182,797,436],{"class":215},[182,799,800],{"class":192},"\"Joined\"",[182,802,442],{"class":215},[182,804,238],{"class":211},[182,806,447],{"class":215},[182,808,365],{"class":192},[182,810,811],{"class":215},"].str.cat(records[",[182,813,376],{"class":192},[182,815,816],{"class":215},"], ",[182,818,819],{"class":602},"sep",[182,821,238],{"class":211},[182,823,824],{"class":192},"\" — \"",[182,826,256],{"class":215},[182,828,829],{"class":602},"na_rep",[182,831,238],{"class":211},[182,833,834],{"class":192},"\"(no name)\"",[182,836,611],{"class":215},[182,838,840,842,844,846,848,850],{"class":184,"line":839},11,[182,841,541],{"class":298},[182,843,544],{"class":215},[182,845,680],{"class":192},[182,847,256],{"class":215},[182,849,800],{"class":192},[182,851,561],{"class":215},[10,853,854,855,857,858,861,862,865,866,868,869,871],{},"The ",[179,856,98],{}," operator behaves like ",[179,859,860],{},"CONCATENATE",": if any part is missing, the whole result is missing.\n",[179,863,864],{},"str.cat"," with ",[179,867,829],{}," behaves like ",[179,870,108],{}," with its ignore-empty flag, supplying a placeholder\ninstead. Choosing between them is a decision about the report — a blank customer name that silently\nblanks the whole label is rarely what anyone wants.",[167,873,875],{"id":874},"trim-upper-proper-and-their-relatives","TRIM, UPPER, PROPER and their relatives",[20,877,29,882,29,885,29,888,29,891,29,897,29,903,29,908,29,914,29,919,29,922,29,925,29,929,29,933,29,937,29,940,29,943,29,946,29,950,29,954,29,957,29,960,29,963,29,967,29,971],{"viewBox":878,"role":23,"ariaLabelledBy":879,"xmlns":27,"style":28},"0 0 760 308",[880,881],"txt-clean-t","txt-clean-d",[31,883,884],{"id":880},"Matching Excel's TRIM exactly",[35,886,887],{"id":881},"Excel's TRIM removes leading and trailing spaces and also collapses internal runs to a single space, so reproducing it takes both a strip and a regular-expression replacement.",[39,889],{"x":41,"y":41,"width":42,"height":890,"fill":44},"308",[39,892],{"x":893,"y":894,"width":895,"height":58,"rx":896,"fill":72,"stroke":73},"22","28","716","10",[898,899],"circle",{"cx":58,"cy":900,"r":901,"fill":902},"54.0","15","#5b5cf0",[55,904,907],{"x":58,"y":905,"style":906},"59.0","font-size:13px;font-weight:700;fill:#ffffff;text-anchor:middle","1",[55,909,913],{"x":910,"y":911,"style":912},"82","50","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","strip the ends",[55,915,918],{"x":910,"y":916,"style":917},"68","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:start",".str.strip() removes leading and trailing space",[39,920],{"x":893,"y":921,"width":895,"height":58,"rx":896,"fill":72,"stroke":73},"90",[898,923],{"cx":58,"cy":924,"r":901,"fill":902},"116.0",[55,926,928],{"x":58,"y":927,"style":906},"121.0","2",[55,930,932],{"x":910,"y":931,"style":912},"112","collapse internal runs",[55,934,936],{"x":910,"y":935,"style":917},"130",".str.replace(r'\\\\s+', ' ') matches TRIM",[39,938],{"x":893,"y":939,"width":895,"height":58,"rx":896,"fill":72,"stroke":73},"152",[898,941],{"cx":58,"cy":942,"r":901,"fill":902},"178.0",[55,944,455],{"x":58,"y":945,"style":906},"183.0",[55,947,949],{"x":910,"y":948,"style":912},"174","normalise the case",[55,951,953],{"x":910,"y":952,"style":917},"192",".str.title() or a known-spellings lookup",[39,955],{"x":893,"y":956,"width":895,"height":58,"rx":896,"fill":72,"stroke":73},"214",[898,958],{"cx":58,"cy":959,"r":901,"fill":902},"240.0",[55,961,523],{"x":58,"y":962,"style":906},"245.0",[55,964,966],{"x":910,"y":965,"style":912},"236","apply to both sides",[55,968,970],{"x":910,"y":969,"style":917},"254","a join only works if both keys were cleaned",[55,972,974],{"x":162,"y":973,"style":164},"296","two spaces and one space are different keys to every join",[172,976,978],{"className":202,"code":977,"language":204,"meta":177,"style":177},"records[\"Customer_Clean\"] = (\n    records[\"Customer\"]\n    .str.strip()                 # =TRIM\n    .str.replace(r\"\\s+\", \" \", regex=True)   # TRIM also collapses internal runs\n    .str.title()                 # =PROPER\n)\n\nrecords[\"Postcode_Norm\"] = records[\"Postcode\"].str.upper().str.replace(\" \", \"\", regex=False)\nprint(records[[\"Customer\", \"Customer_Clean\", \"Postcode\", \"Postcode_Norm\"]])\n",[179,979,980,994,1003,1011,1049,1057,1061,1065,1101],{"__ignoreMap":177},[182,981,982,984,987,989,991],{"class":184,"line":185},[182,983,436],{"class":215},[182,985,986],{"class":192},"\"Customer_Clean\"",[182,988,442],{"class":215},[182,990,238],{"class":211},[182,992,993],{"class":215}," (\n",[182,995,996,999,1001],{"class":184,"line":225},[182,997,998],{"class":215},"    records[",[182,1000,376],{"class":192},[182,1002,458],{"class":215},[182,1004,1005,1008],{"class":184,"line":232},[182,1006,1007],{"class":215},"    .str.strip()                 ",[182,1009,1010],{"class":430},"# =TRIM\n",[182,1012,1013,1016,1019,1022,1025,1027,1029,1031,1034,1036,1039,1041,1043,1046],{"class":184,"line":244},[182,1014,1015],{"class":215},"    .str.replace(",[182,1017,1018],{"class":211},"r",[182,1020,1021],{"class":192},"\"",[182,1023,1024],{"class":298},"\\s",[182,1026,98],{"class":211},[182,1028,1021],{"class":192},[182,1030,256],{"class":215},[182,1032,1033],{"class":192},"\" \"",[182,1035,256],{"class":215},[182,1037,1038],{"class":602},"regex",[182,1040,238],{"class":211},[182,1042,608],{"class":298},[182,1044,1045],{"class":215},")   ",[182,1047,1048],{"class":430},"# TRIM also collapses internal runs\n",[182,1050,1051,1054],{"class":184,"line":275},[182,1052,1053],{"class":215},"    .str.title()                 ",[182,1055,1056],{"class":430},"# =PROPER\n",[182,1058,1059],{"class":184,"line":304},[182,1060,611],{"class":215},[182,1062,1063],{"class":184,"line":331},[182,1064,229],{"emptyLinePlaceholder":228},[182,1066,1067,1069,1072,1074,1076,1078,1080,1083,1085,1087,1090,1092,1094,1096,1099],{"class":184,"line":359},[182,1068,436],{"class":215},[182,1070,1071],{"class":192},"\"Postcode_Norm\"",[182,1073,442],{"class":215},[182,1075,238],{"class":211},[182,1077,447],{"class":215},[182,1079,394],{"class":192},[182,1081,1082],{"class":215},"].str.upper().str.replace(",[182,1084,1033],{"class":192},[182,1086,256],{"class":215},[182,1088,1089],{"class":192},"\"\"",[182,1091,256],{"class":215},[182,1093,1038],{"class":602},[182,1095,238],{"class":211},[182,1097,1098],{"class":298},"False",[182,1100,611],{"class":215},[182,1102,1103,1105,1107,1109,1111,1113,1115,1117,1119,1121],{"class":184,"line":533},[182,1104,541],{"class":298},[182,1106,544],{"class":215},[182,1108,376],{"class":192},[182,1110,256],{"class":215},[182,1112,986],{"class":192},[182,1114,256],{"class":215},[182,1116,394],{"class":192},[182,1118,256],{"class":215},[182,1120,1071],{"class":192},[182,1122,561],{"class":215},[10,1124,1125,1128,1129,1132,1133,1136],{},[179,1126,1127],{},"str.strip()"," alone removes leading and trailing whitespace; Excel's TRIM also collapses runs of\ninternal spaces, which is the second call above. Reproducing both is worth the extra line because\n",[179,1130,1131],{},"\"Acme  Ltd\""," and ",[179,1134,1135],{},"\"Acme Ltd\""," are different keys to every join and group-by that follows.",[10,1138,1139,1142,1143,1146,1147,1150,1151,1154],{},[179,1140,1141],{},"str.title()"," is close to PROPER but not identical — it capitalises after any non-letter, so\n",[179,1144,1145],{},"\"o'brien\""," becomes ",[179,1148,1149],{},"\"O'Brien\""," in both, while ",[179,1152,1153],{},"\"acme (uk) ltd\""," differs in how the parenthesised\npart is handled. For customer names, a lookup of known spellings usually beats any case function.",[167,1156,1158],{"id":1157},"find-search-and-substitute","FIND, SEARCH and SUBSTITUTE",[172,1160,1162],{"className":202,"code":1161,"language":204,"meta":177,"style":177},"# =IFERROR(FIND(\"-\", A2), 0) — position of a substring\nrecords[\"Dash_At\"] = records[\"Reference\"].str.find(\"-\")\n\n# =SEARCH is case-insensitive; the pandas equivalent is a case-insensitive regex\nrecords[\"Has_Inv\"] = records[\"Reference\"].str.contains(\"inv\", case=False, na=False)\n\n# =SUBSTITUTE(A2, \"-\", \"\u002F\")\nrecords[\"Slashed\"] = records[\"Reference\"].str.replace(\"-\", \"\u002F\", regex=False)\n\n# Extract with a pattern — no Excel equivalent short of nested FIND calls\nextracted = records[\"Reference\"].str.extract(r\"^(?P\u003Ckind>[A-Z]{3})-(?P\u003Cyear>\\d{4})-(?P\u003Cserial>\\d+)$\")\nprint(extracted)\n",[179,1163,1164,1169,1191,1195,1200,1241,1245,1250,1285,1289,1294,1369],{"__ignoreMap":177},[182,1165,1166],{"class":184,"line":185},[182,1167,1168],{"class":430},"# =IFERROR(FIND(\"-\", A2), 0) — position of a substring\n",[182,1170,1171,1173,1176,1178,1180,1182,1184,1187,1189],{"class":184,"line":225},[182,1172,436],{"class":215},[182,1174,1175],{"class":192},"\"Dash_At\"",[182,1177,442],{"class":215},[182,1179,238],{"class":211},[182,1181,447],{"class":215},[182,1183,365],{"class":192},[182,1185,1186],{"class":215},"].str.find(",[182,1188,597],{"class":192},[182,1190,611],{"class":215},[182,1192,1193],{"class":184,"line":232},[182,1194,229],{"emptyLinePlaceholder":228},[182,1196,1197],{"class":184,"line":244},[182,1198,1199],{"class":430},"# =SEARCH is case-insensitive; the pandas equivalent is a case-insensitive regex\n",[182,1201,1202,1204,1207,1209,1211,1213,1215,1218,1221,1223,1226,1228,1230,1232,1235,1237,1239],{"class":184,"line":275},[182,1203,436],{"class":215},[182,1205,1206],{"class":192},"\"Has_Inv\"",[182,1208,442],{"class":215},[182,1210,238],{"class":211},[182,1212,447],{"class":215},[182,1214,365],{"class":192},[182,1216,1217],{"class":215},"].str.contains(",[182,1219,1220],{"class":192},"\"inv\"",[182,1222,256],{"class":215},[182,1224,1225],{"class":602},"case",[182,1227,238],{"class":211},[182,1229,1098],{"class":298},[182,1231,256],{"class":215},[182,1233,1234],{"class":602},"na",[182,1236,238],{"class":211},[182,1238,1098],{"class":298},[182,1240,611],{"class":215},[182,1242,1243],{"class":184,"line":304},[182,1244,229],{"emptyLinePlaceholder":228},[182,1246,1247],{"class":184,"line":331},[182,1248,1249],{"class":430},"# =SUBSTITUTE(A2, \"-\", \"\u002F\")\n",[182,1251,1252,1254,1257,1259,1261,1263,1265,1268,1270,1272,1275,1277,1279,1281,1283],{"class":184,"line":359},[182,1253,436],{"class":215},[182,1255,1256],{"class":192},"\"Slashed\"",[182,1258,442],{"class":215},[182,1260,238],{"class":211},[182,1262,447],{"class":215},[182,1264,365],{"class":192},[182,1266,1267],{"class":215},"].str.replace(",[182,1269,597],{"class":192},[182,1271,256],{"class":215},[182,1273,1274],{"class":192},"\"\u002F\"",[182,1276,256],{"class":215},[182,1278,1038],{"class":602},[182,1280,238],{"class":211},[182,1282,1098],{"class":298},[182,1284,611],{"class":215},[182,1286,1287],{"class":184,"line":533},[182,1288,229],{"emptyLinePlaceholder":228},[182,1290,1291],{"class":184,"line":538},[182,1292,1293],{"class":430},"# Extract with a pattern — no Excel equivalent short of nested FIND calls\n",[182,1295,1296,1299,1301,1303,1305,1308,1310,1312,1315,1319,1322,1326,1329,1332,1335,1337,1340,1343,1346,1349,1351,1353,1355,1358,1360,1362,1365,1367],{"class":184,"line":839},[182,1297,1298],{"class":215},"extracted ",[182,1300,238],{"class":211},[182,1302,447],{"class":215},[182,1304,365],{"class":192},[182,1306,1307],{"class":215},"].str.extract(",[182,1309,1018],{"class":211},[182,1311,1021],{"class":192},[182,1313,1314],{"class":298},"^(",[182,1316,1318],{"class":1317},"sx1sP","?P\u003Ckind>",[182,1320,1321],{"class":298},"[",[182,1323,1325],{"class":1324},"sSjpA","A-Z",[182,1327,1328],{"class":298},"]",[182,1330,1331],{"class":211},"{3}",[182,1333,1334],{"class":298},")",[182,1336,488],{"class":192},[182,1338,1339],{"class":298},"(",[182,1341,1342],{"class":1317},"?P\u003Cyear>",[182,1344,1345],{"class":298},"\\d",[182,1347,1348],{"class":211},"{4}",[182,1350,1334],{"class":298},[182,1352,488],{"class":192},[182,1354,1339],{"class":298},[182,1356,1357],{"class":1317},"?P\u003Cserial>",[182,1359,1345],{"class":298},[182,1361,98],{"class":211},[182,1363,1364],{"class":298},")$",[182,1366,1021],{"class":192},[182,1368,611],{"class":215},[182,1370,1372,1374],{"class":184,"line":1371},12,[182,1373,541],{"class":298},[182,1375,1376],{"class":215},"(extracted)\n",[10,1378,1379,1382],{},[179,1380,1381],{},"str.extract"," with named groups is the function that has no spreadsheet counterpart and replaces the\nlargest amount of formula. A reference format that would take three nested FIND and MID calls to\ndecompose becomes one pattern that either matches or produces NaN — and the NaN tells you which rows\ndo not follow the format, which the formula version never would.",[167,1384,1386],{"id":1385},"splitting-an-email-or-an-address-into-parts","Splitting an email or an address into parts",[10,1388,1389],{},"A practical composite: pull the domain out of an email address, and flag the rows where the address\ndoes not look like one.",[172,1391,1393],{"className":202,"code":1392,"language":204,"meta":177,"style":177},"records[\"Domain\"] = records[\"Contact\"].str.split(\"@\").str[-1]\nrecords[\"Valid_Email\"] = records[\"Contact\"].str.match(r\"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$\", na=False)\n\nprint(records.loc[~records[\"Valid_Email\"], [\"Reference\", \"Contact\"]])\n",[179,1394,1395,1424,1501,1505],{"__ignoreMap":177},[182,1396,1397,1399,1402,1404,1406,1408,1410,1412,1415,1418,1420,1422],{"class":184,"line":185},[182,1398,436],{"class":215},[182,1400,1401],{"class":192},"\"Domain\"",[182,1403,442],{"class":215},[182,1405,238],{"class":211},[182,1407,447],{"class":215},[182,1409,385],{"class":192},[182,1411,594],{"class":215},[182,1413,1414],{"class":192},"\"@\"",[182,1416,1417],{"class":215},").str[",[182,1419,488],{"class":211},[182,1421,907],{"class":298},[182,1423,458],{"class":215},[182,1425,1426,1428,1431,1433,1435,1437,1439,1442,1444,1446,1449,1452,1455,1458,1460,1462,1464,1466,1468,1470,1472,1476,1478,1480,1482,1484,1486,1489,1491,1493,1495,1497,1499],{"class":184,"line":225},[182,1427,436],{"class":215},[182,1429,1430],{"class":192},"\"Valid_Email\"",[182,1432,442],{"class":215},[182,1434,238],{"class":211},[182,1436,447],{"class":215},[182,1438,385],{"class":192},[182,1440,1441],{"class":215},"].str.match(",[182,1443,1018],{"class":211},[182,1445,1021],{"class":192},[182,1447,1448],{"class":298},"^[",[182,1450,1451],{"class":211},"^",[182,1453,1454],{"class":1324},"@",[182,1456,1457],{"class":298},"\\s]",[182,1459,98],{"class":211},[182,1461,1454],{"class":192},[182,1463,1321],{"class":298},[182,1465,1451],{"class":211},[182,1467,1454],{"class":1324},[182,1469,1457],{"class":298},[182,1471,98],{"class":211},[182,1473,1475],{"class":1474},"s_b0D","\\.",[182,1477,1321],{"class":298},[182,1479,1451],{"class":211},[182,1481,1454],{"class":1324},[182,1483,1457],{"class":298},[182,1485,98],{"class":211},[182,1487,1488],{"class":298},"$",[182,1490,1021],{"class":192},[182,1492,256],{"class":215},[182,1494,1234],{"class":602},[182,1496,238],{"class":211},[182,1498,1098],{"class":298},[182,1500,611],{"class":215},[182,1502,1503],{"class":184,"line":232},[182,1504,229],{"emptyLinePlaceholder":228},[182,1506,1507,1509,1512,1515,1517,1519,1522,1524,1526,1528],{"class":184,"line":244},[182,1508,541],{"class":298},[182,1510,1511],{"class":215},"(records.loc[",[182,1513,1514],{"class":211},"~",[182,1516,436],{"class":215},[182,1518,1430],{"class":192},[182,1520,1521],{"class":215},"], [",[182,1523,365],{"class":192},[182,1525,256],{"class":215},[182,1527,385],{"class":192},[182,1529,561],{"class":215},[10,1531,1532,1535,1536,1539],{},[179,1533,1534],{},".str.split(\"@\").str[-1]"," chains two string accessors — split produces a column of lists, and the\nsecond ",[179,1537,1538],{},".str"," indexes into each list. That chaining is the idiom to reach for whenever a value needs\ndecomposing and then selecting, and it stays vectorised throughout.",[167,1541,1543],{"id":1542},"numbers-that-have-to-become-text","Numbers that have to become text",[10,1545,1546,1547,1550],{},"Report labels frequently mix numbers into strings, and that is where the translation of Excel's TEXT\nfunction is least direct. Excel's ",[179,1548,1549],{},"TEXT(A2, \"#,##0.00\")"," applies a number format and returns a\nstring; pandas has formatting built into Python rather than into a function, which is more flexible\nand requires saying more.",[172,1552,1554],{"className":202,"code":1553,"language":204,"meta":177,"style":177},"amounts = pd.Series([1200.5, 98000.0, 42.125])\n\n# =TEXT(A2, \"#,##0.00\")\nformatted = amounts.map(lambda v: f\"{v:,.2f}\")\n\n# =TEXT(A2, \"0.0%\")\nrates = pd.Series([0.0725, 0.128, 0.4])\npercentages = rates.map(lambda v: f\"{v:.1%}\")\n\n# =TEXT(A2, \"0000\") — zero-padded identifiers\ncodes = pd.Series([12, 407, 3])\npadded = codes.astype(\"string\").str.zfill(4)\nprint(formatted.tolist(), percentages.tolist(), padded.tolist())\n",[179,1555,1556,1582,1586,1591,1628,1632,1637,1661,1692,1696,1701,1724,1743],{"__ignoreMap":177},[182,1557,1558,1561,1563,1566,1569,1571,1574,1576,1579],{"class":184,"line":185},[182,1559,1560],{"class":215},"amounts ",[182,1562,238],{"class":211},[182,1564,1565],{"class":215}," pd.Series([",[182,1567,1568],{"class":298},"1200.5",[182,1570,256],{"class":215},[182,1572,1573],{"class":298},"98000.0",[182,1575,256],{"class":215},[182,1577,1578],{"class":298},"42.125",[182,1580,1581],{"class":215},"])\n",[182,1583,1584],{"class":184,"line":225},[182,1585,229],{"emptyLinePlaceholder":228},[182,1587,1588],{"class":184,"line":232},[182,1589,1590],{"class":430},"# =TEXT(A2, \"#,##0.00\")\n",[182,1592,1593,1596,1598,1601,1604,1607,1610,1612,1615,1618,1621,1624,1626],{"class":184,"line":244},[182,1594,1595],{"class":215},"formatted ",[182,1597,238],{"class":211},[182,1599,1600],{"class":215}," amounts.map(",[182,1602,1603],{"class":211},"lambda",[182,1605,1606],{"class":215}," v: ",[182,1608,1609],{"class":211},"f",[182,1611,1021],{"class":192},[182,1613,1614],{"class":1324},"{",[182,1616,1617],{"class":215},"v",[182,1619,1620],{"class":211},":,.2f",[182,1622,1623],{"class":1324},"}",[182,1625,1021],{"class":192},[182,1627,611],{"class":215},[182,1629,1630],{"class":184,"line":275},[182,1631,229],{"emptyLinePlaceholder":228},[182,1633,1634],{"class":184,"line":304},[182,1635,1636],{"class":430},"# =TEXT(A2, \"0.0%\")\n",[182,1638,1639,1642,1644,1646,1649,1651,1654,1656,1659],{"class":184,"line":331},[182,1640,1641],{"class":215},"rates ",[182,1643,238],{"class":211},[182,1645,1565],{"class":215},[182,1647,1648],{"class":298},"0.0725",[182,1650,256],{"class":215},[182,1652,1653],{"class":298},"0.128",[182,1655,256],{"class":215},[182,1657,1658],{"class":298},"0.4",[182,1660,1581],{"class":215},[182,1662,1663,1666,1668,1671,1673,1675,1677,1679,1681,1683,1686,1688,1690],{"class":184,"line":359},[182,1664,1665],{"class":215},"percentages ",[182,1667,238],{"class":211},[182,1669,1670],{"class":215}," rates.map(",[182,1672,1603],{"class":211},[182,1674,1606],{"class":215},[182,1676,1609],{"class":211},[182,1678,1021],{"class":192},[182,1680,1614],{"class":1324},[182,1682,1617],{"class":215},[182,1684,1685],{"class":211},":.1%",[182,1687,1623],{"class":1324},[182,1689,1021],{"class":192},[182,1691,611],{"class":215},[182,1693,1694],{"class":184,"line":533},[182,1695,229],{"emptyLinePlaceholder":228},[182,1697,1698],{"class":184,"line":538},[182,1699,1700],{"class":430},"# =TEXT(A2, \"0000\") — zero-padded identifiers\n",[182,1702,1703,1706,1708,1710,1713,1715,1718,1720,1722],{"class":184,"line":839},[182,1704,1705],{"class":215},"codes ",[182,1707,238],{"class":211},[182,1709,1565],{"class":215},[182,1711,1712],{"class":298},"12",[182,1714,256],{"class":215},[182,1716,1717],{"class":298},"407",[182,1719,256],{"class":215},[182,1721,455],{"class":298},[182,1723,1581],{"class":215},[182,1725,1726,1729,1731,1734,1736,1739,1741],{"class":184,"line":1371},[182,1727,1728],{"class":215},"padded ",[182,1730,238],{"class":211},[182,1732,1733],{"class":215}," codes.astype(",[182,1735,371],{"class":192},[182,1737,1738],{"class":215},").str.zfill(",[182,1740,523],{"class":298},[182,1742,611],{"class":215},[182,1744,1746,1748],{"class":184,"line":1745},13,[182,1747,541],{"class":298},[182,1749,1750],{"class":215},"(formatted.tolist(), percentages.tolist(), padded.tolist())\n",[10,1752,1753,1756],{},[179,1754,1755],{},"str.zfill"," is the one worth knowing because it solves the leading-zero problem that Excel creates\nconstantly: an account code stored as a number loses its zeros, and padding it back is a single call\nonce you know the intended width.",[10,1758,1759,1760,18],{},"A caution that applies to all three: producing a formatted string means the value is no longer a\nnumber, so it cannot be summed, sorted numerically or charted. When the destination is a spreadsheet,\nthe better answer is nearly always to write the raw number and set the cell's number format, which\nkeeps both the value and its appearance — the approach in\n",[14,1761,1763],{"href":1762},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fapplying-number-and-date-formats-in-excel\u002Fformat-excel-cells-as-currency-with-python\u002F","Format Excel Cells as Currency with Python",[167,1765,1767],{"id":1766},"cleaning-a-column-before-it-becomes-a-key","Cleaning a column before it becomes a key",[10,1769,1770],{},"Text functions are most often used in service of a join, and the failure mode is subtle: two columns\nthat look identical on screen but differ in an invisible character. Non-breaking spaces from a web\nexport, zero-width characters from a copy-paste, and trailing tabs all survive a visual inspection.",[172,1772,1774],{"className":202,"code":1773,"language":204,"meta":177,"style":177},"import unicodedata\n\ndef normalise_key(series: pd.Series) -> pd.Series:\n    return (\n        series.astype(\"string\")\n              .map(lambda v: unicodedata.normalize(\"NFKC\", v) if pd.notna(v) else v)\n              .str.replace(r\"[ ​\\t]\", \" \", regex=True)\n              .str.replace(r\"\\s+\", \" \", regex=True)\n              .str.strip()\n              .str.casefold()\n    )\n\nrecords[\"Key\"] = normalise_key(records[\"Customer\"])\n",[179,1775,1776,1783,1787,1799,1806,1815,1842,1877,1905,1910,1915,1920,1924],{"__ignoreMap":177},[182,1777,1778,1780],{"class":184,"line":185},[182,1779,212],{"class":211},[182,1781,1782],{"class":215}," unicodedata\n",[182,1784,1785],{"class":184,"line":225},[182,1786,229],{"emptyLinePlaceholder":228},[182,1788,1789,1792,1796],{"class":184,"line":232},[182,1790,1791],{"class":211},"def",[182,1793,1795],{"class":1794},"s_Opv"," normalise_key",[182,1797,1798],{"class":215},"(series: pd.Series) -> pd.Series:\n",[182,1800,1801,1804],{"class":184,"line":244},[182,1802,1803],{"class":211},"    return",[182,1805,993],{"class":215},[182,1807,1808,1811,1813],{"class":184,"line":275},[182,1809,1810],{"class":215},"        series.astype(",[182,1812,371],{"class":192},[182,1814,611],{"class":215},[182,1816,1817,1820,1822,1825,1828,1831,1833,1836,1839],{"class":184,"line":304},[182,1818,1819],{"class":215},"              .map(",[182,1821,1603],{"class":211},[182,1823,1824],{"class":215}," v: unicodedata.normalize(",[182,1826,1827],{"class":192},"\"NFKC\"",[182,1829,1830],{"class":215},", v) ",[182,1832,769],{"class":211},[182,1834,1835],{"class":215}," pd.notna(v) ",[182,1837,1838],{"class":211},"else",[182,1840,1841],{"class":215}," v)\n",[182,1843,1844,1847,1849,1851,1853,1856,1859,1861,1863,1865,1867,1869,1871,1873,1875],{"class":184,"line":331},[182,1845,1846],{"class":215},"              .str.replace(",[182,1848,1018],{"class":211},[182,1850,1021],{"class":192},[182,1852,1321],{"class":298},[182,1854,1855],{"class":1324}," ​",[182,1857,1858],{"class":1474},"\\t",[182,1860,1328],{"class":298},[182,1862,1021],{"class":192},[182,1864,256],{"class":215},[182,1866,1033],{"class":192},[182,1868,256],{"class":215},[182,1870,1038],{"class":602},[182,1872,238],{"class":211},[182,1874,608],{"class":298},[182,1876,611],{"class":215},[182,1878,1879,1881,1883,1885,1887,1889,1891,1893,1895,1897,1899,1901,1903],{"class":184,"line":359},[182,1880,1846],{"class":215},[182,1882,1018],{"class":211},[182,1884,1021],{"class":192},[182,1886,1024],{"class":298},[182,1888,98],{"class":211},[182,1890,1021],{"class":192},[182,1892,256],{"class":215},[182,1894,1033],{"class":192},[182,1896,256],{"class":215},[182,1898,1038],{"class":602},[182,1900,238],{"class":211},[182,1902,608],{"class":298},[182,1904,611],{"class":215},[182,1906,1907],{"class":184,"line":533},[182,1908,1909],{"class":215},"              .str.strip()\n",[182,1911,1912],{"class":184,"line":538},[182,1913,1914],{"class":215},"              .str.casefold()\n",[182,1916,1917],{"class":184,"line":839},[182,1918,1919],{"class":215},"    )\n",[182,1921,1922],{"class":184,"line":1371},[182,1923,229],{"emptyLinePlaceholder":228},[182,1925,1926,1928,1931,1933,1935,1938,1940],{"class":184,"line":1745},[182,1927,436],{"class":215},[182,1929,1930],{"class":192},"\"Key\"",[182,1932,442],{"class":215},[182,1934,238],{"class":211},[182,1936,1937],{"class":215}," normalise_key(records[",[182,1939,376],{"class":192},[182,1941,1581],{"class":215},[10,1943,1944,1947],{},[179,1945,1946],{},"NFKC"," normalisation folds the compatibility characters that cause most of these problems — a\nfull-width letter, a ligature, a non-breaking space — into their ordinary equivalents. Applying the\nsame function to both sides of a join is what makes it reliable; applying it to one side makes the\nmismatch worse.",[167,1949,1951],{"id":1950},"common-pitfalls","Common pitfalls",[1953,1954,1955,1971],"table",{},[1956,1957,1958],"thead",{},[1959,1960,1961,1965,1968],"tr",{},[1962,1963,1964],"th",{},"Symptom",[1962,1966,1967],{},"Cause",[1962,1969,1970],{},"Fix",[1972,1973,1974,1991,2007,2029,2047,2058],"tbody",{},[1959,1975,1976,1982,1985],{},[1977,1978,1979],"td",{},[179,1980,1981],{},"AttributeError: Can only use .str accessor with string values",[1977,1983,1984],{},"The column is numeric or object-typed",[1977,1986,1987,1990],{},[179,1988,1989],{},".astype(\"string\")"," first",[1959,1992,1993,1996,2001],{},[1977,1994,1995],{},"Concatenation produces NaN for some rows",[1977,1997,1998,2000],{},[179,1999,98],{}," propagates missing values",[1977,2002,2003,2004],{},"Use ",[179,2005,2006],{},"str.cat(..., na_rep=\"\")",[1959,2008,2009,2016,2019],{},[1977,2010,2011,2012,2015],{},"Numbers become ",[179,2013,2014],{},"1200.0"," when concatenated",[1977,2017,2018],{},"Float formatting during conversion",[1977,2020,2021,2022,2025,2026],{},"Cast to ",[179,2023,2024],{},"Int64"," before ",[179,2027,2028],{},"astype(\"string\")",[1959,2030,2031,2037,2040],{},[1977,2032,2033,2036],{},[179,2034,2035],{},"str.replace"," does nothing",[1977,2038,2039],{},"The pattern is treated as a regex and contains special characters",[1977,2041,2042,2043,2046],{},"Pass ",[179,2044,2045],{},"regex=False"," for literal replacements",[1959,2048,2049,2052,2055],{},[1977,2050,2051],{},"A join finds no matches after cleaning",[1977,2053,2054],{},"Only one side was normalised",[1977,2056,2057],{},"Apply the same cleaning to both sides",[1959,2059,2060,2066,2069],{},[1977,2061,2062,2065],{},[179,2063,2064],{},"str.contains"," raises on missing values",[1977,2067,2068],{},"NaN in the column",[1977,2070,2042,2071],{},[179,2072,2073],{},"na=False",[167,2075,2077],{"id":2076},"performance-and-scale","Performance and scale",[20,2079,29,2085,29,2088,29,2091,29,2095,29,2101,29,2106,29,2113,29,2119,29,2123,29,2126,29,2130,29,2135,29,2139,29,2142,29,2146,29,2150,29,2154,29,2157,29,2160,29,2164,29,2168],{"viewBox":2080,"role":23,"ariaLabelledBy":2081,"xmlns":27,"style":2084},"0 0 720 240",[2082,2083],"txt-cost-t","txt-cost-d","width:100%;max-width:720px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[31,2086,2087],{"id":2082},"Relative cost of the same extraction over a million rows",[35,2089,2090],{"id":2083},"An apply with a lambda pays a Python call per row, a regular expression is cheaper, a literal replacement cheaper still, and vectorised slicing is the fastest of the four.",[39,2092],{"x":41,"y":41,"width":2093,"height":2094,"fill":44},"720","240",[55,2096,2100],{"x":2097,"y":2098,"style":2099},"20","56","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:start","apply with a lambda",[39,2102],{"x":2103,"y":2104,"width":162,"height":2105,"rx":491,"fill":52,"stroke":53},"200","40","26",[39,2107],{"x":2108,"y":2109,"width":2110,"height":2111,"rx":2112,"fill":147,"stroke":148},"201","41","378.0","24","5",[55,2114,2118],{"x":2115,"y":2116,"style":2117},"592.0","58","font-size:12px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:start","a call per row",[55,2120,2122],{"x":2097,"y":2121,"style":2099},"100","regex replace",[39,2124],{"x":2103,"y":2125,"width":162,"height":2105,"rx":491,"fill":52,"stroke":53},"84",[39,2127],{"x":2108,"y":2128,"width":2129,"height":2111,"rx":2112,"fill":118,"stroke":119},"85","95.3",[55,2131,2134],{"x":2115,"y":2132,"style":2133},"102","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","engine per row",[55,2136,2138],{"x":2097,"y":2137,"style":2099},"144","literal replace",[39,2140],{"x":2103,"y":2141,"width":162,"height":2105,"rx":491,"fill":52,"stroke":53},"128",[39,2143],{"x":2108,"y":2144,"width":2145,"height":2111,"rx":2112,"fill":72,"stroke":73},"129","41.9",[55,2147,2045],{"x":2115,"y":2148,"style":2149},"146","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start",[55,2151,2153],{"x":2097,"y":2152,"style":2099},"188","slicing",[39,2155],{"x":2103,"y":2156,"width":162,"height":2105,"rx":491,"fill":52,"stroke":53},"172",[39,2158],{"x":2108,"y":2159,"width":894,"height":2111,"rx":2112,"fill":72,"stroke":73},"173",[55,2161,2163],{"x":2115,"y":2162,"style":2149},"190","vectorised",[55,2165,2167],{"x":2097,"y":2097,"style":2166},"font-size:11.5px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start","relative cost",[55,2169,2172],{"x":2170,"y":2171,"style":164},"360.0","230","strings are the slowest columns; the method still matters",[10,2174,2175,2176,2178,2179,2182,2183,2186],{},"String operations are the slowest column operations in pandas, because strings are Python objects\nrather than packed numbers. Three choices make a large difference. Prefer ",[179,2177,2045],{}," when the\npattern is a literal — the regex engine is a real cost. Chain methods rather than calling ",[179,2180,2181],{},"apply","\nwith a lambda, which pays a Python call per row. And convert a column with few distinct values to\n",[179,2184,2185],{},"category"," before joining or grouping on it.",[172,2188,2190],{"className":202,"code":2189,"language":204,"meta":177,"style":177},"# Slower: a Python call per row\nrecords[\"Doc_Type\"] = records[\"Reference\"].apply(lambda ref: ref[:3])\n\n# Faster: vectorised slicing\nrecords[\"Doc_Type\"] = records[\"Reference\"].str[:3]\n",[179,2191,2192,2197,2223,2227,2232],{"__ignoreMap":177},[182,2193,2194],{"class":184,"line":185},[182,2195,2196],{"class":430},"# Slower: a Python call per row\n",[182,2198,2199,2201,2203,2205,2207,2209,2211,2214,2216,2219,2221],{"class":184,"line":225},[182,2200,436],{"class":215},[182,2202,439],{"class":192},[182,2204,442],{"class":215},[182,2206,238],{"class":211},[182,2208,447],{"class":215},[182,2210,365],{"class":192},[182,2212,2213],{"class":215},"].apply(",[182,2215,1603],{"class":211},[182,2217,2218],{"class":215}," ref: ref[:",[182,2220,455],{"class":298},[182,2222,1581],{"class":215},[182,2224,2225],{"class":184,"line":232},[182,2226,229],{"emptyLinePlaceholder":228},[182,2228,2229],{"class":184,"line":244},[182,2230,2231],{"class":430},"# Faster: vectorised slicing\n",[182,2233,2234,2236,2238,2240,2242,2244,2246,2248,2250],{"class":184,"line":275},[182,2235,436],{"class":215},[182,2237,439],{"class":192},[182,2239,442],{"class":215},[182,2241,238],{"class":211},[182,2243,447],{"class":215},[182,2245,365],{"class":192},[182,2247,452],{"class":215},[182,2249,455],{"class":298},[182,2251,458],{"class":215},[10,2253,2254,2255,18],{},"Where a text column drives repeated grouping — a region, a status, a document type — the categorical\nconversion pays for itself immediately, and it also shrinks the memory the frame occupies, which\nmatters on the large exports covered in\n",[14,2256,2258],{"href":2257},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002F","Working with Large Excel Files in Python",[167,2260,2262],{"id":2261},"conclusion","Conclusion",[10,2264,2265,2266,2268,2269,2271,2272,2274,2275,2278,2279,2282,2283,2285],{},"LEFT, RIGHT and MID are all ",[179,2267,82],{},"; CONCATENATE is ",[179,2270,98],{}," and TEXTJOIN is ",[179,2273,864],{}," with a\nplaceholder; TRIM needs both ",[179,2276,2277],{},"strip"," and a whitespace collapse to match Excel exactly. Beyond the\ndirect translations, ",[179,2280,2281],{},"str.split(expand=True)"," is Text to Columns in a line and ",[179,2284,1381],{}," with\nnamed groups replaces whole stacks of nested FIND and MID formulas — while telling you which rows do\nnot fit the pattern.",[167,2287,2289],{"id":2288},"frequently-asked-questions","Frequently asked questions",[10,2291,2292,2300,2301,2304,2305,2308,2309,2312],{},[2293,2294,2295,2296,2299],"strong",{},"Why is .str",[182,2297,2298],{},"0:3"," used instead of a LEFT function?","\nBecause a pandas string column supports Python slicing directly, and slicing expresses LEFT, RIGHT and MID with one syntax instead of three functions. .str",[182,2302,2303],{},":3"," is LEFT, .str",[182,2306,2307],{},"-3:"," is RIGHT, and .str",[182,2310,2311],{},"2:5"," is MID with a start and length.",[10,2314,2315,2318],{},[2293,2316,2317],{},"What is the difference between + and str.cat for joining columns?","\nPlus produces NaN if any part is missing, exactly as CONCATENATE would produce an error. str.cat with na_rep supplies a placeholder instead, which is usually what a report needs.",[10,2320,2321,2324],{},[2293,2322,2323],{},"How do I split a column into several, like Text to Columns?","\nstr.split with expand=True returns a DataFrame of the parts, which you can assign to several columns at once. Pass n to limit the number of splits when the last part may itself contain the separator.",[10,2326,2327,2330],{},[2293,2328,2329],{},"Do these methods work on a numeric column?","\nNo — .str requires a string dtype. Convert first with .astype('string'), and be aware that a float column converts to values like '1200.0' rather than '1200', so cast to a nullable integer type before converting if that matters.",[167,2332,2334],{"id":2333},"related","Related",[2336,2337,2338,2345,2352,2359,2366],"ul",{},[2339,2340,2341,2342,2344],"li",{},"Up one level: ",[14,2343,17],{"href":16}," — the wider function map.",[2339,2346,2347,2351],{},[14,2348,2350],{"href":2349},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fstrip-whitespace-and-normalise-text-columns-with-pandas\u002F","Strip Whitespace and Normalise Text Columns with Pandas"," — the cleaning pass these functions belong to.",[2339,2353,2354,2358],{},[14,2355,2357],{"href":2356},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-excel-text-columns-to-numbers-with-pandas\u002F","Convert Excel Text Columns to Numbers with Pandas"," — turning extracted text back into numbers.",[2339,2360,2361,2365],{},[14,2362,2364],{"href":2363},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fsplit-one-excel-sheet-into-multiple-files-by-value\u002F","Split One Excel Sheet into Multiple Files by Value"," — using an extracted key to partition a workbook.",[2339,2367,2368,2372],{},[14,2369,2371],{"href":2370},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-data-with-pandera-schemas\u002F","Validate Excel Data with Pandera Schemas"," — asserting the patterns these extractions rely on.",[2374,2375,2376],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sx1sP, html code.shiki .sx1sP{--shiki-default:#22863A;--shiki-dark:#72F088}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":177,"searchDepth":225,"depth":225,"links":2378},[2379,2380,2381,2382,2383,2384,2385,2386,2387,2388,2389,2390,2391],{"id":169,"depth":225,"text":170},{"id":419,"depth":225,"text":420},{"id":662,"depth":225,"text":663},{"id":874,"depth":225,"text":875},{"id":1157,"depth":225,"text":1158},{"id":1385,"depth":225,"text":1386},{"id":1542,"depth":225,"text":1543},{"id":1766,"depth":225,"text":1767},{"id":1950,"depth":225,"text":1951},{"id":2076,"depth":225,"text":2077},{"id":2261,"depth":225,"text":2262},{"id":2288,"depth":225,"text":2289},{"id":2333,"depth":225,"text":2334},"2026-09-04","Slicing replaces LEFT, RIGHT and MID; + and str.cat replace CONCATENATE and TEXTJOIN. Plus split, extract and the regex work that has no formula equivalent.","md",[2396,2399,2401,2403],{"q":2397,"a":2398},"Why is .str[0:3] used instead of a LEFT function?","Because a pandas string column supports Python slicing directly, and slicing expresses LEFT, RIGHT and MID with one syntax instead of three functions. .str[:3] is LEFT, .str[-3:] is RIGHT, and .str[2:5] is MID with a start and length.",{"q":2317,"a":2400},"Plus produces NaN if any part is missing, exactly as CONCATENATE would produce an error. str.cat with na_rep supplies a placeholder instead, which is usually what a report needs.",{"q":2323,"a":2402},"str.split with expand=True returns a DataFrame of the parts, which you can assign to several columns at once. Pass n to limit the number of splits when the last part may itself contain the separator.",{"q":2329,"a":2404},"No — .str requires a string dtype. Convert first with .astype('string'), and be aware that a float column converts to values like '1200.0' rather than '1200', so cast to a nullable integer type before converting if that matters.",{"breadcrumb":2406},[2407,2410,2413],{"name":2408,"item":2409},"Home","\u002F",{"name":2411,"item":2412},"Advanced Data Transformation and Cleaning","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":17,"item":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fexcel-text-functions-left-right-mid-and-concat-in-pandas",{"title":5,"description":2416},"Translate Excel text formulas to pandas string methods: slicing for LEFT\u002FRIGHT\u002FMID, str.cat for TEXTJOIN, TRIM's two steps, and str.extract in place of nested FIND calls.","excel-text-functions-left-right-mid-and-concat-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fexcel-text-functions-left-right-mid-and-concat-in-pandas\u002Findex","how-to","qacnsyt7VUVY5jEGf89dZJoeHCq9XM0-8KdONdApIPg",[2422,2426],{"title":2423,"path":2424,"stem":2425,"children":-1},"Excel IF Formulas as pandas Conditional Columns","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fexcel-if-formulas-as-pandas-conditional-columns","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fexcel-if-formulas-as-pandas-conditional-columns\u002Findex",{"title":2427,"path":2428,"stem":2429,"children":-1},"INDEX MATCH Equivalent in pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Findex-match-equivalent-in-pandas","advanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Findex-match-equivalent-in-pandas\u002Findex",1788710154434]