[{"data":1,"prerenderedAt":2128},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fmap-free-text-job-titles-to-standard-categories":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fmap-free-text-job-titles-to-standard-categories":2094,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fmap-free-text-job-titles-to-standard-categories":2103,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fmap-free-text-job-titles-to-standard-categories":2108,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fmap-free-text-job-titles-to-standard-categories":2109},{"id":4,"title":5,"body":6,"dateModified":2059,"datePublished":2059,"description":2060,"extension":2061,"faq":2062,"keyword":2071,"kind":2072,"meta":2073,"navigation":125,"path":2083,"seo":2084,"slug":2087,"stem":2088,"topics":2089,"type":2092,"__hash__":2093},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fmap-free-text-job-titles-to-standard-categories\u002Findex.md","Map Free-Text Job Titles to Standard Categories",{"type":7,"value":8,"toc":2047},"minimark",[9,40,45,74,85,89,543,588,592,849,878,1030,1034,1221,1244,1272,1338,1342,1673,1692,1898,1902,1918,1922,1968,1972,1975,1979,1985,1997,2006,2012,2016,2043],[10,11,12,13,17,18,17,21,17,24,17,27,17,30,33,34,39],"p",{},"A headcount report by job family needs every employee in a family: Finance, Engineering, Sales, Operations. The HR export, though, has a free-text title field, and three hundred people have two hundred different titles — ",[14,15,16],"em",{},"Sr. Software Eng.",", ",[14,19,20],{},"Software Engineer II",[14,22,23],{},"Snr Developer",[14,25,26],{},"Head of FP&A",[14,28,29],{},"Accounts Payable Clerk",[14,31,32],{},"Warehouse op."," A pivot table on that column is useless. Mapping titles to standard categories by hand works once and then rots, because new titles keep arriving. A small rule-based mapper in pandas does the bulk of the work, is easy to correct, and says clearly which titles it could not place. This page builds one: it normalises the text and expands common abbreviations, applies an ordered table of keyword rules that lives in an Excel sheet the HR team can edit, falls back to fuzzy matching against titles mapped before, pulls seniority out as a separate column, and lists whatever is left for a person to decide. It belongs to ",[35,36,38],"a",{"href":37},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002F","Cleaning Excel Data with pandas",".",[41,42,44],"h2",{"id":43},"prerequisites","Prerequisites",[46,47,52],"pre",{"className":48,"code":49,"language":50,"meta":51,"style":51},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[53,54,55],"code",{"__ignoreMap":51},[56,57,60,64,68,71],"span",{"class":58,"line":59},"line",1,[56,61,63],{"class":62},"sMTad","pip",[56,65,67],{"class":66},"srMev"," install",[56,69,70],{"class":66}," pandas",[56,72,73],{"class":66}," openpyxl\n",[10,75,76,77,80,81,84],{},"Two inputs: the staff list with a free-text ",[14,78,79],{},"Title"," column, and a rules sheet with one row per rule — a priority, a regular expression and the job family it maps to. Fuzzy matching uses the standard library's ",[53,82,83],{},"difflib",", so nothing else needs installing.",[41,86,88],{"id":87},"step-1-normalise-the-text-first","Step 1 — Normalise the text first",[46,90,94],{"className":91,"code":92,"language":93,"meta":51,"style":51},"language-python shiki shiki-themes github-light github-dark-high-contrast","import re\nimport pandas as pd\n\nstaff = pd.read_excel(\"staff.xlsx\")\nABBREV = {r\"\\bsr\\b|\\bsnr\\b\": \"senior\", r\"\\bjr\\b|\\bjnr\\b\": \"junior\", r\"\\beng\\b\": \"engineer\", r\"\\bmgr\\b\": \"manager\",\n          r\"\\brep\\b\": \"representative\", r\"\\bop\\b\": \"operative\", r\"\\bvp\\b\": \"vice president\",\n          r\"\\bchief financial officer\\b\": \"cfo\"}\n\ndef normalise(title):\n    t = title.lower()\n    t = re.sub(r\"[.\\-–,\u002F]\", \" \", t)                    # punctuation to spaces (keep & for fp&a)\n    for pattern, full in ABBREV.items():\n        t = re.sub(pattern, full, t)\n    return re.sub(r\"\\s+\", \" \", t).strip()\n\nstaff[\"Normalised\"] = staff[\"Title\"].map(normalise)\nprint(staff[[\"Title\", \"Normalised\"]].head(8).to_string(index=False))\n","python",[53,95,96,106,120,127,145,262,326,350,355,368,379,423,441,452,479,484,507],{"__ignoreMap":51},[56,97,98,102],{"class":58,"line":59},[56,99,101],{"class":100},"s-kum","import",[56,103,105],{"class":104},"skGVy"," re\n",[56,107,109,111,114,117],{"class":58,"line":108},2,[56,110,101],{"class":100},[56,112,113],{"class":104}," pandas ",[56,115,116],{"class":100},"as",[56,118,119],{"class":104}," pd\n",[56,121,123],{"class":58,"line":122},3,[56,124,126],{"emptyLinePlaceholder":125},true,"\n",[56,128,130,133,136,139,142],{"class":58,"line":129},4,[56,131,132],{"class":104},"staff ",[56,134,135],{"class":100},"=",[56,137,138],{"class":104}," pd.read_excel(",[56,140,141],{"class":66},"\"staff.xlsx\"",[56,143,144],{"class":104},")\n",[56,146,148,152,155,158,161,164,167,170,172,175,177,180,182,184,187,190,192,194,196,198,201,203,205,207,210,212,214,216,219,221,223,225,227,230,232,234,236,239,241,243,245,247,250,252,254,256,259],{"class":58,"line":147},5,[56,149,151],{"class":150},"sP0c6","ABBREV",[56,153,154],{"class":100}," =",[56,156,157],{"class":104}," {",[56,159,160],{"class":100},"r",[56,162,163],{"class":66},"\"",[56,165,166],{"class":150},"\\b",[56,168,169],{"class":66},"sr",[56,171,166],{"class":150},[56,173,174],{"class":100},"|",[56,176,166],{"class":150},[56,178,179],{"class":66},"snr",[56,181,166],{"class":150},[56,183,163],{"class":66},[56,185,186],{"class":104},": ",[56,188,189],{"class":66},"\"senior\"",[56,191,17],{"class":104},[56,193,160],{"class":100},[56,195,163],{"class":66},[56,197,166],{"class":150},[56,199,200],{"class":66},"jr",[56,202,166],{"class":150},[56,204,174],{"class":100},[56,206,166],{"class":150},[56,208,209],{"class":66},"jnr",[56,211,166],{"class":150},[56,213,163],{"class":66},[56,215,186],{"class":104},[56,217,218],{"class":66},"\"junior\"",[56,220,17],{"class":104},[56,222,160],{"class":100},[56,224,163],{"class":66},[56,226,166],{"class":150},[56,228,229],{"class":66},"eng",[56,231,166],{"class":150},[56,233,163],{"class":66},[56,235,186],{"class":104},[56,237,238],{"class":66},"\"engineer\"",[56,240,17],{"class":104},[56,242,160],{"class":100},[56,244,163],{"class":66},[56,246,166],{"class":150},[56,248,249],{"class":66},"mgr",[56,251,166],{"class":150},[56,253,163],{"class":66},[56,255,186],{"class":104},[56,257,258],{"class":66},"\"manager\"",[56,260,261],{"class":104},",\n",[56,263,265,268,270,272,275,277,279,281,284,286,288,290,292,295,297,299,301,304,306,308,310,312,315,317,319,321,324],{"class":58,"line":264},6,[56,266,267],{"class":100},"          r",[56,269,163],{"class":66},[56,271,166],{"class":150},[56,273,274],{"class":66},"rep",[56,276,166],{"class":150},[56,278,163],{"class":66},[56,280,186],{"class":104},[56,282,283],{"class":66},"\"representative\"",[56,285,17],{"class":104},[56,287,160],{"class":100},[56,289,163],{"class":66},[56,291,166],{"class":150},[56,293,294],{"class":66},"op",[56,296,166],{"class":150},[56,298,163],{"class":66},[56,300,186],{"class":104},[56,302,303],{"class":66},"\"operative\"",[56,305,17],{"class":104},[56,307,160],{"class":100},[56,309,163],{"class":66},[56,311,166],{"class":150},[56,313,314],{"class":66},"vp",[56,316,166],{"class":150},[56,318,163],{"class":66},[56,320,186],{"class":104},[56,322,323],{"class":66},"\"vice president\"",[56,325,261],{"class":104},[56,327,329,331,333,335,338,340,342,344,347],{"class":58,"line":328},7,[56,330,267],{"class":100},[56,332,163],{"class":66},[56,334,166],{"class":150},[56,336,337],{"class":66},"chief financial officer",[56,339,166],{"class":150},[56,341,163],{"class":66},[56,343,186],{"class":104},[56,345,346],{"class":66},"\"cfo\"",[56,348,349],{"class":104},"}\n",[56,351,353],{"class":58,"line":352},8,[56,354,126],{"emptyLinePlaceholder":125},[56,356,358,361,365],{"class":58,"line":357},9,[56,359,360],{"class":100},"def",[56,362,364],{"class":363},"s_Opv"," normalise",[56,366,367],{"class":104},"(title):\n",[56,369,371,374,376],{"class":58,"line":370},10,[56,372,373],{"class":104},"    t ",[56,375,135],{"class":100},[56,377,378],{"class":104}," title.lower()\n",[56,380,382,384,386,389,391,393,396,399,403,406,409,411,413,416,419],{"class":58,"line":381},11,[56,383,373],{"class":104},[56,385,135],{"class":100},[56,387,388],{"class":104}," re.sub(",[56,390,160],{"class":100},[56,392,163],{"class":66},[56,394,395],{"class":150},"[",[56,397,39],{"class":398},"sSjpA",[56,400,402],{"class":401},"s_b0D","\\-",[56,404,405],{"class":398},"–,\u002F",[56,407,408],{"class":150},"]",[56,410,163],{"class":66},[56,412,17],{"class":104},[56,414,415],{"class":66},"\" \"",[56,417,418],{"class":104},", t)                    ",[56,420,422],{"class":421},"s-wDw","# punctuation to spaces (keep & for fp&a)\n",[56,424,426,429,432,435,438],{"class":58,"line":425},12,[56,427,428],{"class":100},"    for",[56,430,431],{"class":104}," pattern, full ",[56,433,434],{"class":100},"in",[56,436,437],{"class":150}," ABBREV",[56,439,440],{"class":104},".items():\n",[56,442,444,447,449],{"class":58,"line":443},13,[56,445,446],{"class":104},"        t ",[56,448,135],{"class":100},[56,450,451],{"class":104}," re.sub(pattern, full, t)\n",[56,453,455,458,460,462,464,467,470,472,474,476],{"class":58,"line":454},14,[56,456,457],{"class":100},"    return",[56,459,388],{"class":104},[56,461,160],{"class":100},[56,463,163],{"class":66},[56,465,466],{"class":150},"\\s",[56,468,469],{"class":100},"+",[56,471,163],{"class":66},[56,473,17],{"class":104},[56,475,415],{"class":66},[56,477,478],{"class":104},", t).strip()\n",[56,480,482],{"class":58,"line":481},15,[56,483,126],{"emptyLinePlaceholder":125},[56,485,487,490,493,496,498,501,504],{"class":58,"line":486},16,[56,488,489],{"class":104},"staff[",[56,491,492],{"class":66},"\"Normalised\"",[56,494,495],{"class":104},"] ",[56,497,135],{"class":100},[56,499,500],{"class":104}," staff[",[56,502,503],{"class":66},"\"Title\"",[56,505,506],{"class":104},"].map(normalise)\n",[56,508,510,513,516,518,520,522,525,528,531,535,537,540],{"class":58,"line":509},17,[56,511,512],{"class":150},"print",[56,514,515],{"class":104},"(staff[[",[56,517,503],{"class":66},[56,519,17],{"class":104},[56,521,492],{"class":66},[56,523,524],{"class":104},"]].head(",[56,526,527],{"class":150},"8",[56,529,530],{"class":104},").to_string(",[56,532,534],{"class":533},"sa561","index",[56,536,135],{"class":100},[56,538,539],{"class":150},"False",[56,541,542],{"class":104},"))\n",[10,544,545,546,549,550,552,553,549,555,558,559,17,562,17,565,17,568,17,571,17,574,576,577,579,580,583,584,587],{},"Every later step is simpler if the titles are in one consistent form. Lower-casing, turning punctuation into spaces and collapsing runs of spaces deal with ",[14,547,548],{},"Sr."," versus ",[14,551,169],{}," and ",[14,554,32],{},[14,556,557],{},"warehouse op",". A small dictionary expands the abbreviations people actually use — ",[14,560,561],{},"Sr",[14,563,564],{},"Snr",[14,566,567],{},"Eng",[14,569,570],{},"Mgr",[14,572,573],{},"Rep",[14,575,294],{}," — with word boundaries so ",[14,578,294],{}," inside ",[14,581,582],{},"operations"," is not touched. Long titles that have well-known short forms, such as ",[14,585,586],{},"Chief Financial Officer",", can be mapped to a single token that a rule can match. The normalised title is kept as a separate column; the original stays as it was for the report.",[41,589,591],{"id":590},"step-2-seniority-as-its-own-column","Step 2 — Seniority as its own column",[46,593,595],{"className":91,"code":594,"language":93,"meta":51,"style":51},"SENIORITY = [(r\"\\b(chief|cfo|vice president|head of|director)\\b\", \"Executive\"),\n             (r\"\\b(senior|lead|principal|manager|supervisor)\\b|\\bii\\b|\\biii\\b\", \"Senior\"),\n             (r\"\\b(junior|graduate|assistant|trainee|clerk|coordinator)\\b\", \"Junior\")]\n\ndef seniority(t):\n    for pattern, level in SENIORITY:\n        if re.search(pattern, t):\n            return level\n    return \"Mid\"\n\nstaff[\"Seniority\"] = staff[\"Normalised\"].map(seniority)\nprint(staff[\"Seniority\"].value_counts().to_dict())\n",[53,596,597,650,713,763,767,777,792,800,808,815,819,837],{"__ignoreMap":51},[56,598,599,602,604,607,609,611,614,617,619,622,624,627,629,632,634,637,640,642,644,647],{"class":58,"line":59},[56,600,601],{"class":150},"SENIORITY",[56,603,154],{"class":100},[56,605,606],{"class":104}," [(",[56,608,160],{"class":100},[56,610,163],{"class":66},[56,612,613],{"class":150},"\\b(",[56,615,616],{"class":66},"chief",[56,618,174],{"class":100},[56,620,621],{"class":66},"cfo",[56,623,174],{"class":100},[56,625,626],{"class":66},"vice president",[56,628,174],{"class":100},[56,630,631],{"class":66},"head of",[56,633,174],{"class":100},[56,635,636],{"class":66},"director",[56,638,639],{"class":150},")\\b",[56,641,163],{"class":66},[56,643,17],{"class":104},[56,645,646],{"class":66},"\"Executive\"",[56,648,649],{"class":104},"),\n",[56,651,652,655,657,659,661,664,666,669,671,674,676,679,681,684,686,688,690,693,695,697,699,702,704,706,708,711],{"class":58,"line":108},[56,653,654],{"class":104},"             (",[56,656,160],{"class":100},[56,658,163],{"class":66},[56,660,613],{"class":150},[56,662,663],{"class":66},"senior",[56,665,174],{"class":100},[56,667,668],{"class":66},"lead",[56,670,174],{"class":100},[56,672,673],{"class":66},"principal",[56,675,174],{"class":100},[56,677,678],{"class":66},"manager",[56,680,174],{"class":100},[56,682,683],{"class":66},"supervisor",[56,685,639],{"class":150},[56,687,174],{"class":100},[56,689,166],{"class":150},[56,691,692],{"class":66},"ii",[56,694,166],{"class":150},[56,696,174],{"class":100},[56,698,166],{"class":150},[56,700,701],{"class":66},"iii",[56,703,166],{"class":150},[56,705,163],{"class":66},[56,707,17],{"class":104},[56,709,710],{"class":66},"\"Senior\"",[56,712,649],{"class":104},[56,714,715,717,719,721,723,726,728,731,733,736,738,741,743,746,748,751,753,755,757,760],{"class":58,"line":122},[56,716,654],{"class":104},[56,718,160],{"class":100},[56,720,163],{"class":66},[56,722,613],{"class":150},[56,724,725],{"class":66},"junior",[56,727,174],{"class":100},[56,729,730],{"class":66},"graduate",[56,732,174],{"class":100},[56,734,735],{"class":66},"assistant",[56,737,174],{"class":100},[56,739,740],{"class":66},"trainee",[56,742,174],{"class":100},[56,744,745],{"class":66},"clerk",[56,747,174],{"class":100},[56,749,750],{"class":66},"coordinator",[56,752,639],{"class":150},[56,754,163],{"class":66},[56,756,17],{"class":104},[56,758,759],{"class":66},"\"Junior\"",[56,761,762],{"class":104},")]\n",[56,764,765],{"class":58,"line":129},[56,766,126],{"emptyLinePlaceholder":125},[56,768,769,771,774],{"class":58,"line":147},[56,770,360],{"class":100},[56,772,773],{"class":363}," seniority",[56,775,776],{"class":104},"(t):\n",[56,778,779,781,784,786,789],{"class":58,"line":264},[56,780,428],{"class":100},[56,782,783],{"class":104}," pattern, level ",[56,785,434],{"class":100},[56,787,788],{"class":150}," SENIORITY",[56,790,791],{"class":104},":\n",[56,793,794,797],{"class":58,"line":328},[56,795,796],{"class":100},"        if",[56,798,799],{"class":104}," re.search(pattern, t):\n",[56,801,802,805],{"class":58,"line":352},[56,803,804],{"class":100},"            return",[56,806,807],{"class":104}," level\n",[56,809,810,812],{"class":58,"line":357},[56,811,457],{"class":100},[56,813,814],{"class":66}," \"Mid\"\n",[56,816,817],{"class":58,"line":370},[56,818,126],{"emptyLinePlaceholder":125},[56,820,821,823,826,828,830,832,834],{"class":58,"line":381},[56,822,489],{"class":104},[56,824,825],{"class":66},"\"Seniority\"",[56,827,495],{"class":104},[56,829,135],{"class":100},[56,831,500],{"class":104},[56,833,492],{"class":66},[56,835,836],{"class":104},"].map(seniority)\n",[56,838,839,841,844,846],{"class":58,"line":425},[56,840,512],{"class":150},[56,842,843],{"class":104},"(staff[",[56,845,825],{"class":66},[56,847,848],{"class":104},"].value_counts().to_dict())\n",[10,850,851,852,855,856,859,860,863,864,866,867,870,871,552,874,877],{},"Job titles mix two things: the ",[14,853,854],{},"function"," (software, payroll, warehouse) and the ",[14,857,858],{},"level"," (senior, head of, junior). Separating them gives two clean dimensions for reporting — headcount by family, and by level within each family — and stops the family rules from needing a variant for every level. The seniority rules are checked in order, executive first, so ",[14,861,862],{},"Head of Sales"," is an executive even though it also contains no ",[14,865,663],{},". Anything without a level word is treated as ",[14,868,869],{},"Mid",". Grade suffixes such as ",[14,872,873],{},"II",[14,875,876],{},"III"," count as senior here; organisations with formal grade ladders usually map those to their own levels instead, which is another short rule list.",[879,880,888,889,888,893,888,897,888,905,888,915,888,922,888,925,888,930,888,934,888,940,888,945,888,949,888,955,888,959,888,963,888,967,888,970,888,975,888,980,888,983,888,987,888,990,888,995,888,1000,888,1004,888,1007,888,1011,888,1014,888,1019,888,1024,888,1027],"svg",{"viewBox":881,"role":882,"ariaLabelledBy":883,"xmlns":886,"style":887},"4 0 752 126","img",[884,885],"jt-pipeline-t","jt-pipeline-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  ",[890,891,892],"title",{"id":884},"From a free-text title to family and level",[894,895,896],"desc",{"id":885},"A title is normalised, abbreviations expanded, then seniority is taken from level words. The family comes from the first matching keyword rule in priority order; if none matches, the title is compared with titles mapped before; if that is not close enough, it goes to the review list.",[898,899],"rect",{"x":900,"y":901,"width":902,"height":903,"fill":904},"4","0","752","126","#ffffff",[898,906],{"x":907,"y":908,"width":909,"height":910,"rx":911,"fill":912,"stroke":913,"style":914},"20.0","16.0","115.2","94.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[916,917,921],"text",{"x":918,"y":919,"style":920},"77.6","41.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Free-text",[916,923,890],{"x":918,"y":924,"style":920},"59.0",[916,926,929],{"x":918,"y":927,"style":928},"78.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","e.g. Sr.",[916,931,933],{"x":918,"y":932,"style":928},"96.0","Software Eng.",[58,935],{"x1":936,"y1":937,"x2":938,"y2":937,"stroke":939,"style":914},"140.2","63.0","162.2","var(--brand,#5b5cf0)",[941,942],"polygon",{"points":943,"fill":944},"166.2,63.0 157.2,68.0 157.2,58.0","#5b5cf0",[898,946],{"x":947,"y":908,"width":909,"height":910,"rx":911,"fill":948,"stroke":939,"style":914},"171.2","#f0f4ff",[916,950,954],{"x":951,"y":952,"style":953},"228.8","50.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Normalise",[916,956,958],{"x":951,"y":957,"style":928},"69.8","senior software",[916,960,962],{"x":951,"y":961,"style":928},"87.2","engineer",[58,964],{"x1":965,"y1":937,"x2":966,"y2":937,"stroke":939,"style":914},"291.4","313.4",[941,968],{"points":969,"fill":944},"317.4,63.0 308.4,68.0 308.4,58.0",[898,971],{"x":972,"y":908,"width":909,"height":910,"rx":911,"fill":973,"stroke":974,"style":914},"322.4","#fdefd8","var(--gold,#b4740a)",[916,976,979],{"x":977,"y":924,"style":978},"380.0","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Seniority",[916,981,982],{"x":977,"y":927,"style":928},"Senior",[58,984],{"x1":985,"y1":937,"x2":986,"y2":937,"stroke":939,"style":914},"442.6","464.6",[941,988],{"points":989,"fill":944},"468.6,63.0 459.6,68.0 459.6,58.0",[898,991],{"x":992,"y":908,"width":909,"height":910,"rx":911,"fill":993,"stroke":994,"style":914},"473.6","#d9f4f1","var(--teal,#0f9488)",[916,996,999],{"x":997,"y":952,"style":998},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Keyword",[916,1001,1003],{"x":997,"y":1002,"style":998},"67.8","rules",[916,1005,1006],{"x":997,"y":961,"style":928},"→ Engineering",[58,1008],{"x1":1009,"y1":937,"x2":1010,"y2":937,"stroke":939,"style":914},"593.8","615.8",[941,1012],{"points":1013,"fill":944},"619.8,63.0 610.8,68.0 610.8,58.0",[898,1015],{"x":1016,"y":908,"width":909,"height":910,"rx":911,"fill":1017,"stroke":1018,"style":914},"624.8","#fce9e9","var(--danger,#dc2626)",[916,1020,1023],{"x":1021,"y":952,"style":1022},"682.4","font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Fuzzy match",[916,1025,1026],{"x":1021,"y":1002,"style":1022},"\u002F review",[916,1028,1029],{"x":1021,"y":961,"style":928},"if no rule fits",[41,1031,1033],{"id":1032},"step-3-ordered-keyword-rules-from-an-excel-sheet","Step 3 — Ordered keyword rules from an Excel sheet",[46,1035,1037],{"className":91,"code":1036,"language":93,"meta":51,"style":51},"rules = pd.read_excel(\"title_rules.xlsx\").sort_values(\"Priority\")\ncompiled = [(re.compile(p), fam) for p, fam in zip(rules[\"Pattern\"], rules[\"Family\"])]\n\ndef family_by_rule(t):\n    for rx, fam in compiled:\n        if rx.search(t):\n            return fam\n    return None\n\nstaff[\"Family\"] = staff[\"Normalised\"].map(family_by_rule)\nstaff[\"Method\"] = staff[\"Family\"].notna().map({True: \"rule\", False: None})\nprint(staff.groupby(\"Family\", dropna=False).size().to_dict())\n",[53,1038,1039,1059,1095,1099,1108,1120,1127,1134,1141,1145,1162,1200],{"__ignoreMap":51},[56,1040,1041,1044,1046,1048,1051,1054,1057],{"class":58,"line":59},[56,1042,1043],{"class":104},"rules ",[56,1045,135],{"class":100},[56,1047,138],{"class":104},[56,1049,1050],{"class":66},"\"title_rules.xlsx\"",[56,1052,1053],{"class":104},").sort_values(",[56,1055,1056],{"class":66},"\"Priority\"",[56,1058,144],{"class":104},[56,1060,1061,1064,1066,1069,1072,1075,1077,1080,1083,1086,1089,1092],{"class":58,"line":108},[56,1062,1063],{"class":104},"compiled ",[56,1065,135],{"class":100},[56,1067,1068],{"class":104}," [(re.compile(p), fam) ",[56,1070,1071],{"class":100},"for",[56,1073,1074],{"class":104}," p, fam ",[56,1076,434],{"class":100},[56,1078,1079],{"class":150}," zip",[56,1081,1082],{"class":104},"(rules[",[56,1084,1085],{"class":66},"\"Pattern\"",[56,1087,1088],{"class":104},"], rules[",[56,1090,1091],{"class":66},"\"Family\"",[56,1093,1094],{"class":104},"])]\n",[56,1096,1097],{"class":58,"line":122},[56,1098,126],{"emptyLinePlaceholder":125},[56,1100,1101,1103,1106],{"class":58,"line":129},[56,1102,360],{"class":100},[56,1104,1105],{"class":363}," family_by_rule",[56,1107,776],{"class":104},[56,1109,1110,1112,1115,1117],{"class":58,"line":147},[56,1111,428],{"class":100},[56,1113,1114],{"class":104}," rx, fam ",[56,1116,434],{"class":100},[56,1118,1119],{"class":104}," compiled:\n",[56,1121,1122,1124],{"class":58,"line":264},[56,1123,796],{"class":100},[56,1125,1126],{"class":104}," rx.search(t):\n",[56,1128,1129,1131],{"class":58,"line":328},[56,1130,804],{"class":100},[56,1132,1133],{"class":104}," fam\n",[56,1135,1136,1138],{"class":58,"line":352},[56,1137,457],{"class":100},[56,1139,1140],{"class":150}," None\n",[56,1142,1143],{"class":58,"line":357},[56,1144,126],{"emptyLinePlaceholder":125},[56,1146,1147,1149,1151,1153,1155,1157,1159],{"class":58,"line":370},[56,1148,489],{"class":104},[56,1150,1091],{"class":66},[56,1152,495],{"class":104},[56,1154,135],{"class":100},[56,1156,500],{"class":104},[56,1158,492],{"class":66},[56,1160,1161],{"class":104},"].map(family_by_rule)\n",[56,1163,1164,1166,1169,1171,1173,1175,1177,1180,1183,1185,1188,1190,1192,1194,1197],{"class":58,"line":381},[56,1165,489],{"class":104},[56,1167,1168],{"class":66},"\"Method\"",[56,1170,495],{"class":104},[56,1172,135],{"class":100},[56,1174,500],{"class":104},[56,1176,1091],{"class":66},[56,1178,1179],{"class":104},"].notna().map({",[56,1181,1182],{"class":150},"True",[56,1184,186],{"class":104},[56,1186,1187],{"class":66},"\"rule\"",[56,1189,17],{"class":104},[56,1191,539],{"class":150},[56,1193,186],{"class":104},[56,1195,1196],{"class":150},"None",[56,1198,1199],{"class":104},"})\n",[56,1201,1202,1204,1207,1209,1211,1214,1216,1218],{"class":58,"line":425},[56,1203,512],{"class":150},[56,1205,1206],{"class":104},"(staff.groupby(",[56,1208,1091],{"class":66},[56,1210,17],{"class":104},[56,1212,1213],{"class":533},"dropna",[56,1215,135],{"class":100},[56,1217,539],{"class":150},[56,1219,1220],{"class":104},").size().to_dict())\n",[10,1222,1223,1224,1227,1228,1231,1232,1235,1236,1239,1240,1243],{},"The rules live in an Excel sheet — priority, pattern, family — because they are business knowledge that HR, not a developer, should maintain. The first rule that matches wins, which is why priority matters: ",[14,1225,1226],{},"data engineer"," belongs to Engineering, so the Engineering rule comes before the Data & Analytics rule that would otherwise claim anything with ",[14,1229,1230],{},"data"," in it. Patterns use word boundaries so ",[14,1233,1234],{},"hr"," matches ",[14,1237,1238],{},"HR Manager"," but not ",[14,1241,1242],{},"three",". Reading the rules from the sheet at run time means a correction is an edit to the sheet and a rerun, with no code change. In this staff list the rules place 26 of the 31 titles straight away; the other five — an office manager, a facilities assistant, a customer success manager, a product designer and a brand ambassador — match no keyword and go on to the next step.",[10,1245,1246,1247,552,1250,1253,1254,1257,1258,1261,1262,552,1265,1268,1269,1271],{},"Rule order caught this example out the first time it was run. With Finance at priority 3 and Sales at 4, ",[14,1248,1249],{},"Account Executive",[14,1251,1252],{},"Key Account Manager"," both landed in Finance, because the Finance pattern includes ",[14,1255,1256],{},"account"," for ",[14,1259,1260],{},"Accounts Payable"," and it was checked first. Swapping the two priorities — the specific ",[14,1263,1264],{},"account executive",[14,1266,1267],{},"account manager"," before the general ",[14,1270,1256],{}," — put them in Sales without touching any pattern. That is the typical maintenance task for a rule table: when a title lands in the wrong family, look for an earlier, broader rule that claimed it.",[879,1273,888,1278,888,1281,888,1284,888,1287,888,1292,888,1298,888,1304,888,1309,888,1313,888,1317,888,1320,888,1325,888,1329,888,1332,888,1335],{"viewBox":1274,"role":882,"ariaLabelledBy":1275,"xmlns":886,"style":887},"4 0 752 173",[1276,1277],"jt-order-t","jt-order-d",[890,1279,1280],{"id":1276},"Why rule order matters",[894,1282,1283],{"id":1277},"With the broad Finance rule first, Account Executive and Key Account Manager match the word account and are mapped to Finance. With the specific Sales rule first, they match account executive and account manager and are mapped to Sales, while Accounts Payable Clerk still reaches Finance.",[898,1285],{"x":900,"y":901,"width":902,"height":1286,"fill":904},"173",[898,1288],{"x":907,"y":908,"width":1289,"height":1290,"rx":1291,"fill":1017,"stroke":1018,"style":914},"351.0","141.0","14",[916,1293,1297],{"x":1294,"y":1295,"style":1296},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Finance rule first",[58,1299],{"x1":1300,"y1":1301,"x2":1302,"y2":1301,"stroke":1018,"style":1303},"36.0","57.0","355.0","stroke-width:1px",[916,1305,1308],{"x":1294,"y":1306,"style":1307},"77.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Account Executive → Finance",[916,1310,1312],{"x":1294,"y":1311,"style":1307},"103.0","Key Account Manager → Finance",[916,1314,1316],{"x":1294,"y":1315,"style":1307},"129.0","wrong, via the word account",[898,1318],{"x":1319,"y":908,"width":1289,"height":1290,"rx":1291,"fill":993,"stroke":994,"style":914},"389.0",[916,1321,1324],{"x":1322,"y":1295,"style":1323},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Sales rule first",[58,1326],{"x1":1327,"y1":1301,"x2":1328,"y2":1301,"stroke":994,"style":1303},"405.0","724.0",[916,1330,1331],{"x":1322,"y":1306,"style":1307},"Account Executive → Sales",[916,1333,1334],{"x":1322,"y":1311,"style":1307},"Key Account Manager → Sales",[916,1336,1337],{"x":1322,"y":1315,"style":1307},"Accounts Payable Clerk → Finance",[41,1339,1341],{"id":1340},"step-4-fuzzy-matching-for-the-rest-then-a-review-list","Step 4 — Fuzzy matching for the rest, then a review list",[46,1343,1345],{"className":91,"code":1344,"language":93,"meta":51,"style":51},"import difflib\n\nknown = {\"office manager\": \"Facilities\", \"facilities assistant\": \"Facilities\",\n         \"customer success manager\": \"Sales\", \"product designer\": \"Engineering\"}     # titles mapped in earlier runs\n\ndef family_by_similarity(t, cutoff=0.85):\n    match = difflib.get_close_matches(t, list(known), n=1, cutoff=cutoff)\n    return (known[match[0]], match[0]) if match else (None, None)\n\ntodo = staff[\"Family\"].isna()\nfound = staff.loc[todo, \"Normalised\"].map(family_by_similarity)\nstaff.loc[todo, \"Family\"] = found.map(lambda x: x[0])\nstaff.loc[todo & staff[\"Family\"].notna(), \"Method\"] = \"similar to known title\"\nreview = staff[staff[\"Family\"].isna()][[\"Employee\", \"Title\", \"Normalised\", \"Seniority\"]]\n\nprint(staff[[\"Title\", \"Family\", \"Seniority\", \"Method\"]].to_string(index=False))\nprint(\"for review:\", review[\"Title\"].tolist())\n",[53,1346,1347,1354,1358,1386,1412,1416,1434,1468,1505,1509,1523,1538,1563,1587,1620,1624,1655],{"__ignoreMap":51},[56,1348,1349,1351],{"class":58,"line":59},[56,1350,101],{"class":100},[56,1352,1353],{"class":104}," difflib\n",[56,1355,1356],{"class":58,"line":108},[56,1357,126],{"emptyLinePlaceholder":125},[56,1359,1360,1363,1365,1367,1370,1372,1375,1377,1380,1382,1384],{"class":58,"line":122},[56,1361,1362],{"class":104},"known ",[56,1364,135],{"class":100},[56,1366,157],{"class":104},[56,1368,1369],{"class":66},"\"office manager\"",[56,1371,186],{"class":104},[56,1373,1374],{"class":66},"\"Facilities\"",[56,1376,17],{"class":104},[56,1378,1379],{"class":66},"\"facilities assistant\"",[56,1381,186],{"class":104},[56,1383,1374],{"class":66},[56,1385,261],{"class":104},[56,1387,1388,1391,1393,1396,1398,1401,1403,1406,1409],{"class":58,"line":129},[56,1389,1390],{"class":66},"         \"customer success manager\"",[56,1392,186],{"class":104},[56,1394,1395],{"class":66},"\"Sales\"",[56,1397,17],{"class":104},[56,1399,1400],{"class":66},"\"product designer\"",[56,1402,186],{"class":104},[56,1404,1405],{"class":66},"\"Engineering\"",[56,1407,1408],{"class":104},"}     ",[56,1410,1411],{"class":421},"# titles mapped in earlier runs\n",[56,1413,1414],{"class":58,"line":147},[56,1415,126],{"emptyLinePlaceholder":125},[56,1417,1418,1420,1423,1426,1428,1431],{"class":58,"line":264},[56,1419,360],{"class":100},[56,1421,1422],{"class":363}," family_by_similarity",[56,1424,1425],{"class":104},"(t, cutoff",[56,1427,135],{"class":100},[56,1429,1430],{"class":150},"0.85",[56,1432,1433],{"class":104},"):\n",[56,1435,1436,1439,1441,1444,1447,1450,1453,1455,1458,1460,1463,1465],{"class":58,"line":328},[56,1437,1438],{"class":104},"    match ",[56,1440,135],{"class":100},[56,1442,1443],{"class":104}," difflib.get_close_matches(t, ",[56,1445,1446],{"class":150},"list",[56,1448,1449],{"class":104},"(known), ",[56,1451,1452],{"class":533},"n",[56,1454,135],{"class":100},[56,1456,1457],{"class":150},"1",[56,1459,17],{"class":104},[56,1461,1462],{"class":533},"cutoff",[56,1464,135],{"class":100},[56,1466,1467],{"class":104},"cutoff)\n",[56,1469,1470,1472,1475,1477,1480,1482,1485,1488,1491,1494,1497,1499,1501,1503],{"class":58,"line":352},[56,1471,457],{"class":100},[56,1473,1474],{"class":104}," (known[match[",[56,1476,901],{"class":150},[56,1478,1479],{"class":104},"]], match[",[56,1481,901],{"class":150},[56,1483,1484],{"class":104},"]) ",[56,1486,1487],{"class":100},"if",[56,1489,1490],{"class":104}," match ",[56,1492,1493],{"class":100},"else",[56,1495,1496],{"class":104}," (",[56,1498,1196],{"class":150},[56,1500,17],{"class":104},[56,1502,1196],{"class":150},[56,1504,144],{"class":104},[56,1506,1507],{"class":58,"line":357},[56,1508,126],{"emptyLinePlaceholder":125},[56,1510,1511,1514,1516,1518,1520],{"class":58,"line":370},[56,1512,1513],{"class":104},"todo ",[56,1515,135],{"class":100},[56,1517,500],{"class":104},[56,1519,1091],{"class":66},[56,1521,1522],{"class":104},"].isna()\n",[56,1524,1525,1528,1530,1533,1535],{"class":58,"line":381},[56,1526,1527],{"class":104},"found ",[56,1529,135],{"class":100},[56,1531,1532],{"class":104}," staff.loc[todo, ",[56,1534,492],{"class":66},[56,1536,1537],{"class":104},"].map(family_by_similarity)\n",[56,1539,1540,1543,1545,1547,1549,1552,1555,1558,1560],{"class":58,"line":425},[56,1541,1542],{"class":104},"staff.loc[todo, ",[56,1544,1091],{"class":66},[56,1546,495],{"class":104},[56,1548,135],{"class":100},[56,1550,1551],{"class":104}," found.map(",[56,1553,1554],{"class":100},"lambda",[56,1556,1557],{"class":104}," x: x[",[56,1559,901],{"class":150},[56,1561,1562],{"class":104},"])\n",[56,1564,1565,1568,1571,1573,1575,1578,1580,1582,1584],{"class":58,"line":443},[56,1566,1567],{"class":104},"staff.loc[todo ",[56,1569,1570],{"class":100},"&",[56,1572,500],{"class":104},[56,1574,1091],{"class":66},[56,1576,1577],{"class":104},"].notna(), ",[56,1579,1168],{"class":66},[56,1581,495],{"class":104},[56,1583,135],{"class":100},[56,1585,1586],{"class":66}," \"similar to known title\"\n",[56,1588,1589,1592,1594,1597,1599,1602,1605,1607,1609,1611,1613,1615,1617],{"class":58,"line":454},[56,1590,1591],{"class":104},"review ",[56,1593,135],{"class":100},[56,1595,1596],{"class":104}," staff[staff[",[56,1598,1091],{"class":66},[56,1600,1601],{"class":104},"].isna()][[",[56,1603,1604],{"class":66},"\"Employee\"",[56,1606,17],{"class":104},[56,1608,503],{"class":66},[56,1610,17],{"class":104},[56,1612,492],{"class":66},[56,1614,17],{"class":104},[56,1616,825],{"class":66},[56,1618,1619],{"class":104},"]]\n",[56,1621,1622],{"class":58,"line":481},[56,1623,126],{"emptyLinePlaceholder":125},[56,1625,1626,1628,1630,1632,1634,1636,1638,1640,1642,1644,1647,1649,1651,1653],{"class":58,"line":486},[56,1627,512],{"class":150},[56,1629,515],{"class":104},[56,1631,503],{"class":66},[56,1633,17],{"class":104},[56,1635,1091],{"class":66},[56,1637,17],{"class":104},[56,1639,825],{"class":66},[56,1641,17],{"class":104},[56,1643,1168],{"class":66},[56,1645,1646],{"class":104},"]].to_string(",[56,1648,534],{"class":533},[56,1650,135],{"class":100},[56,1652,539],{"class":150},[56,1654,542],{"class":104},[56,1656,1657,1659,1662,1665,1668,1670],{"class":58,"line":509},[56,1658,512],{"class":150},[56,1660,1661],{"class":104},"(",[56,1663,1664],{"class":66},"\"for review:\"",[56,1666,1667],{"class":104},", review[",[56,1669,503],{"class":66},[56,1671,1672],{"class":104},"].tolist())\n",[10,1674,1675,1676,1679,1680,1683,1684,1687,1688,1691],{},"Titles no rule matches are compared with a dictionary of titles that were mapped in earlier runs — the institutional memory of the mapper — using ",[53,1677,1678],{},"difflib.get_close_matches",", which scores how similar two strings are. Only close matches count: at a cutoff of 0.85, ",[14,1681,1682],{},"customer success manager"," matches the known title of the same name exactly, while an unfamiliar title matches nothing. The result records ",[14,1685,1686],{},"how"," each title was mapped — by rule or by similarity — which matters when someone asks why a person is counted in a family. Whatever is still unmapped goes on a review list — here a single title, ",[14,1689,1690],{},"Brand Ambassador",", which no rule covers and which resembles no known title closely enough. Each decision taken there is added either as a rule, if it generalises, or as a known title, so the review list shrinks run by run.",[879,1693,888,1699,888,1702,888,1705,888,1710,888,1715,888,1719,888,1724,888,1728,888,1732,888,1736,888,1740,888,1744,888,1748,888,1752,888,1755,888,1758,888,1761,888,1763,888,1766,888,1768,888,1770,888,1772,888,1775,888,1778,888,1782,888,1784,888,1786,888,1788,888,1791,888,1793,888,1795,888,1797,888,1800,888,1802,888,1806,888,1808,888,1810,888,1812,888,1815,888,1817,888,1820,888,1822,888,1824,888,1827,888,1830,888,1832,888,1834,888,1836,888,1839,888,1841,888,1843,888,1845,888,1847,888,1850,888,1854,888,1856,888,1859,888,1861,888,1864,888,1866,888,1868,888,1870,888,1874,888,1877,888,1881,888,1883,888,1886,888,1888,888,1890,888,1892,888,1894,888,1896],{"viewBox":1694,"role":882,"ariaLabelledBy":1695,"xmlns":886,"style":1698},"77 0 606 224",[1696,1697],"jt-results-t","jt-results-d","width:100%;max-width:606px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[890,1700,1701],{"id":1696},"A sample of mapped titles",[894,1703,1704],{"id":1697},"Sr. Software Eng. maps to Engineering at Senior level by rule. Head of FP&A maps to Finance at Executive level. Warehouse op. maps to Operations. Customer Success Mgr maps to Sales by similarity to a known title. Shift Supervisor - Warehouse maps to Operations at Senior level.",[898,1706],{"x":1707,"y":901,"width":1708,"height":1709,"fill":904},"77","606","224",[898,1711],{"x":1712,"y":908,"width":1713,"height":1714,"rx":901,"fill":912,"stroke":913,"style":1303},"93.5","32.0","24.0",[898,1716],{"x":1717,"y":908,"width":1718,"height":1714,"rx":901,"fill":912,"stroke":913,"style":1303},"125.5","206.6",[916,1720,1723],{"x":951,"y":1721,"style":1722},"33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[898,1725],{"x":1726,"y":908,"width":1727,"height":1714,"rx":901,"fill":912,"stroke":913,"style":1303},"332.1","98.1",[916,1729,1731],{"x":1730,"y":1721,"style":1722},"381.1","B",[898,1733],{"x":1734,"y":908,"width":1735,"height":1714,"rx":901,"fill":912,"stroke":913,"style":1303},"430.2","84.8",[916,1737,1739],{"x":1738,"y":1721,"style":1722},"472.6","C",[898,1741],{"x":1742,"y":908,"width":1743,"height":1714,"rx":901,"fill":912,"stroke":913,"style":1303},"515.0","151.5",[916,1745,1747],{"x":1746,"y":1721,"style":1722},"590.8","D",[898,1749],{"x":1712,"y":1750,"width":1713,"height":1751,"rx":901,"fill":912,"stroke":913,"style":1303},"40.0","28.0",[916,1753,1457],{"x":1754,"y":924,"style":1722},"109.5",[898,1756],{"x":1717,"y":1750,"width":1718,"height":1751,"rx":901,"fill":948,"stroke":939,"style":1757},"stroke-width:1.5px",[916,1759,79],{"x":951,"y":924,"style":1760},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[898,1762],{"x":1726,"y":1750,"width":1727,"height":1751,"rx":901,"fill":948,"stroke":939,"style":1757},[916,1764,1765],{"x":1730,"y":924,"style":1760},"Family",[898,1767],{"x":1734,"y":1750,"width":1735,"height":1751,"rx":901,"fill":948,"stroke":939,"style":1757},[916,1769,979],{"x":1738,"y":924,"style":1760},[898,1771],{"x":1742,"y":1750,"width":1743,"height":1751,"rx":901,"fill":948,"stroke":939,"style":1757},[916,1773,1774],{"x":1746,"y":924,"style":1760},"Method",[898,1776],{"x":1712,"y":1777,"width":1713,"height":1751,"rx":901,"fill":912,"stroke":913,"style":1303},"68.0",[916,1779,1781],{"x":1754,"y":1780,"style":1722},"87.0","2",[898,1783],{"x":1717,"y":1777,"width":1718,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1785,16],{"x":951,"y":1780,"style":1307},[898,1787],{"x":1726,"y":1777,"width":1727,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1789,1790],{"x":1730,"y":1780,"style":1307},"Engineering",[898,1792],{"x":1734,"y":1777,"width":1735,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1794,982],{"x":1738,"y":1780,"style":1307},[898,1796],{"x":1742,"y":1777,"width":1743,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1798,1799],{"x":1746,"y":1780,"style":1307},"rule",[898,1801],{"x":1712,"y":932,"width":1713,"height":1751,"rx":901,"fill":912,"stroke":913,"style":1303},[916,1803,1805],{"x":1754,"y":1804,"style":1722},"115.0","3",[898,1807],{"x":1717,"y":932,"width":1718,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1809,26],{"x":951,"y":1804,"style":1307},[898,1811],{"x":1726,"y":932,"width":1727,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1813,1814],{"x":1730,"y":1804,"style":1307},"Finance",[898,1816],{"x":1734,"y":932,"width":1735,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1818,1819],{"x":1738,"y":1804,"style":1307},"Executive",[898,1821],{"x":1742,"y":932,"width":1743,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1823,1799],{"x":1746,"y":1804,"style":1307},[898,1825],{"x":1712,"y":1826,"width":1713,"height":1751,"rx":901,"fill":912,"stroke":913,"style":1303},"124.0",[916,1828,900],{"x":1754,"y":1829,"style":1722},"143.0",[898,1831],{"x":1717,"y":1826,"width":1718,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1833,32],{"x":951,"y":1829,"style":1307},[898,1835],{"x":1726,"y":1826,"width":1727,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1837,1838],{"x":1730,"y":1829,"style":1307},"Operations",[898,1840],{"x":1734,"y":1826,"width":1735,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1842,869],{"x":1738,"y":1829,"style":1307},[898,1844],{"x":1742,"y":1826,"width":1743,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1846,1799],{"x":1746,"y":1829,"style":1307},[898,1848],{"x":1712,"y":1849,"width":1713,"height":1751,"rx":901,"fill":912,"stroke":913,"style":1303},"152.0",[916,1851,1853],{"x":1754,"y":1852,"style":1722},"171.0","5",[898,1855],{"x":1717,"y":1849,"width":1718,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1857,1858],{"x":951,"y":1852,"style":1307},"Customer Success Mgr",[898,1860],{"x":1726,"y":1849,"width":1727,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1862,1863],{"x":1730,"y":1852,"style":1307},"Sales",[898,1865],{"x":1734,"y":1849,"width":1735,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1867,982],{"x":1738,"y":1852,"style":1307},[898,1869],{"x":1742,"y":1849,"width":1743,"height":1751,"rx":901,"fill":973,"stroke":974,"style":1757},[916,1871,1873],{"x":1746,"y":1852,"style":1872},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","similar to known title",[898,1875],{"x":1712,"y":1876,"width":1713,"height":1751,"rx":901,"fill":912,"stroke":913,"style":1303},"180.0",[916,1878,1880],{"x":1754,"y":1879,"style":1722},"199.0","6",[898,1882],{"x":1717,"y":1876,"width":1718,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1884,1885],{"x":951,"y":1879,"style":1307},"Shift Supervisor - Warehouse",[898,1887],{"x":1726,"y":1876,"width":1727,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1889,1838],{"x":1730,"y":1879,"style":1307},[898,1891],{"x":1734,"y":1876,"width":1735,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1893,982],{"x":1738,"y":1879,"style":1307},[898,1895],{"x":1742,"y":1876,"width":1743,"height":1751,"rx":901,"fill":904,"stroke":913,"style":1303},[916,1897,1799],{"x":1746,"y":1879,"style":1307},[41,1899,1901],{"id":1900},"write-the-mapping-and-the-review-list","Write the mapping and the review list",[10,1903,1904,1905,1908,1909,1913,1914,1917],{},"The output workbook should hold three things: the staff list with the original title, family, seniority and method; the review list; and a copy of the rules used, so a report can always be traced back to the rule set that produced it. Writing them with ",[53,1906,1907],{},"pd.ExcelWriter"," to separate sheets takes a few lines. When the mapping feeds a recurring headcount report, as in ",[35,1910,1912],{"href":1911},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-headcount-and-attrition-report\u002F","Build a Headcount and Attrition Report in Excel with Python",", run the mapper first and stop the report if the review list is longer than an agreed threshold — a report that silently drops a dozen people into ",[14,1915,1916],{},"Other"," is worse than one that waits a day.",[41,1919,1921],{"id":1920},"common-pitfalls","Common pitfalls",[1923,1924,1925,1933,1945,1956,1962],"ul",{},[1926,1927,1928,1932],"li",{},[1929,1930,1931],"strong",{},"Matching raw titles."," Normalise case, punctuation and abbreviations first.",[1926,1934,1935,1938,1939,1941,1942,1944],{},[1929,1936,1937],{},"Rules in the wrong order."," Put specific rules (",[14,1940,1226],{},") before general ones (",[14,1943,1230],{},").",[1926,1946,1947,1950,1951,1953,1954,39],{},[1929,1948,1949],{},"Patterns without word boundaries."," ",[53,1952,1234],{}," must not match ",[14,1955,1242],{},[1926,1957,1958,1961],{},[1929,1959,1960],{},"Mixing level and function."," Extract seniority separately from the job family.",[1926,1963,1964,1967],{},[1929,1965,1966],{},"Silent \"Other\"."," Report unmapped titles and feed decisions back into the rules.",[41,1969,1971],{"id":1970},"conclusion","Conclusion",[10,1973,1974],{},"To map free-text job titles to standard categories, normalise them and expand abbreviations, take seniority from level words as its own column, apply an ordered table of keyword rules kept in Excel so the business can maintain it, fall back to close matches against titles mapped before, and send the remainder to a review list. Record how each title was mapped, and turn every review decision into a rule or a known title so the mapper improves with each run.",[41,1976,1978],{"id":1977},"frequently-asked-questions","Frequently asked questions",[10,1980,1981,1984],{},[1929,1982,1983],{},"How do I standardise job titles in pandas?","\nNormalise the text, then apply an ordered list of regular-expression rules mapping keywords to job families, falling back to fuzzy matching for the rest.",[10,1986,1987,1990,1991,1994,1995,39],{},[1929,1988,1989],{},"How do I handle abbreviations like Sr and Mgr?","\nExpand them with word-boundary regular expressions before matching, for example ",[53,1992,1993],{},"\\bsr\\b"," to ",[14,1996,663],{},[10,1998,1999,2002,2003,2005],{},[1929,2000,2001],{},"How do I fuzzy match job titles without extra libraries?","\nUse ",[53,2004,1678],{}," against a list of known titles, with a high cutoff such as 0.85.",[10,2007,2008,2011],{},[1929,2009,2010],{},"Why keep the mapping rules in Excel?","\nSo the people who know the job families can correct and extend them without changing code.",[41,2013,2015],{"id":2014},"related","Related",[1923,2017,2018,2024,2031,2038],{},[1926,2019,2020,2021,2023],{},"Up one level: ",[35,2022,38],{"href":37}," — cleaning workflows.",[1926,2025,2026,2030],{},[35,2027,2029],{"href":2028},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fremove-duplicate-words-and-extra-punctuation-from-text\u002F","Remove Duplicate Words and Extra Punctuation from Text"," — tidying text first.",[1926,2032,2033,2037],{},[35,2034,2036],{"href":2035},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmerging-and-joining-excel-dataframes\u002Ffuzzy-match-names-between-two-excel-files-with-python\u002F","Fuzzy Match Names Between Two Excel Files with Python"," — fuzzy matching in depth.",[1926,2039,2040,2042],{},[35,2041,1912],{"href":1911}," — where the families are used.",[2044,2045,2046],"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_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}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-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}",{"title":51,"searchDepth":108,"depth":108,"links":2048},[2049,2050,2051,2052,2053,2054,2055,2056,2057,2058],{"id":43,"depth":108,"text":44},{"id":87,"depth":108,"text":88},{"id":590,"depth":108,"text":591},{"id":1032,"depth":108,"text":1033},{"id":1340,"depth":108,"text":1341},{"id":1900,"depth":108,"text":1901},{"id":1920,"depth":108,"text":1921},{"id":1970,"depth":108,"text":1971},{"id":1977,"depth":108,"text":1978},{"id":2014,"depth":108,"text":2015},"2026-10-02","Turn hundreds of free-text job titles from an HR export or a contact list into a handful of standard job families and seniority levels with pandas: normalise the text, expand abbreviations, apply an ordered keyword rule table kept in Excel, fall back to fuzzy matching against known titles, separate seniority from function, and report every title that still needs a human decision.","md",[2063,2065,2067,2069],{"q":1983,"a":2064},"Normalise the text, then apply an ordered list of regular-expression rules mapping keywords to job families, falling back to fuzzy matching for the rest.",{"q":1989,"a":2066},"Expand them with word-boundary regular expressions before matching, for example \\bsr\\b to *senior*.",{"q":2001,"a":2068},"Use difflib.get_close_matches against a list of known titles, with a high cutoff such as 0.85.",{"q":2010,"a":2070},"So the people who know the job families can correct and extend them without changing code.","standardise job titles python",null,{"breadcrumb":2074},[2075,2078,2081],{"name":2076,"item":2077},"Home","\u002F",{"name":2079,"item":2080},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":2082,"item":37},"Cleaning Excel Data With Pandas: A Production Workflow","\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fmap-free-text-job-titles-to-standard-categories",{"title":2085,"description":2086},"Map Free-Text Job Titles to Standard Categories with pandas","Standardise job titles with pandas: normalisation, abbreviation expansion, an ordered keyword rule table in Excel, fuzzy matching with difflib, seniority levels and a list of unmapped titles.","map-free-text-job-titles-to-standard-categories","advanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fmap-free-text-job-titles-to-standard-categories\u002Findex",[2090,2091],"data-cleaning","joins-and-lookups","how-to","c66TcQ03tgMSBz11vix2cbBw6I0-dQSy0q8LqPcGpNc",[2095,2099],{"title":2096,"path":2097,"stem":2098,"children":-1},"Keep the Latest Record per Key When Deduplicating with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fkeep-the-latest-record-per-key-with-pandas","advanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fkeep-the-latest-record-per-key-with-pandas\u002Findex",{"title":2100,"path":2101,"stem":2102,"children":-1},"Normalise Postcodes and ZIP Codes with pandas","\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fnormalise-postcodes-and-zip-codes-with-pandas","advanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fnormalise-postcodes-and-zip-codes-with-pandas\u002Findex",[2104,2106],{"path":2105,"title":2082},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas",{"path":2107,"title":2079},"\u002Fadvanced-data-transformation-and-cleaning",[],[2110,2113,2116,2119,2122,2125],{"path":2111,"title":2112,"kind":2092},"\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":2114,"title":2115,"kind":2092},"\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":2117,"title":2118,"kind":2092},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fclean-mixed-currency-amounts-into-amount-and-currency-columns","Clean Mixed-Currency Amounts into Amount and Currency Columns",{"path":2120,"title":2121,"kind":2092},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fcoalesce-several-excel-columns-into-one-with-pandas","Coalesce Several Excel Columns into One with pandas",{"path":2123,"title":2124,"kind":2092},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fconvert-currency-strings-to-numbers-with-pandas","Convert Currency Strings Like \"$1,234.56\" and \"(500)\" to Numbers with pandas",{"path":2126,"title":2127,"kind":2092},"\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",1790972080821]