-5

I have a function that takes a string as an input and tries to extract the name and surname. It is a combination of NER and regex to try to extract the names present. Is there a better or more efficient way to do it? It struggles mainly with compound names i.e first name + middle name + surname. A user could input John Michael Smith and the code struggles with determining what part belongs to which.

def extract_name(self, text: str):
        all_names = []

        # Method 1: Named Entity Recognition (NER)
        try:
            tokens = nltk.word_tokenize(text)
            pos_tags = nltk.pos_tag(tokens)
            named_entities = nltk.ne_chunk(pos_tags)

            for chunk in named_entities:
                if hasattr(chunk, 'label') and chunk.label() == 'PERSON':
                    name = ' '.join([token for token, pos in chunk.leaves()])
                    all_names.append(name)
        except Exception as e:
            logger.debug(f"NER extraction failed: {e}")

        # Method 2: POS Tagging + Pattern Recognition
        # Define action verbs to skip (must match before combining into names)
        action_verbs_lower = {
            'change', 'remove', 'update', 'delete', 'add', 'create',
            'show', 'find', 'get', 'view', 'display', 'list',
            'fetch', 'retrieve', 'pull', 'access', 'lookup', 'search', 'locate', 'bring',
            'mark', 'set', 'record', 'edit', 'modify', 'alter', 'revise',
            'erase', 'drop', 'register', 'enroll'
        }

        try:
            tokens = nltk.word_tokenize(text)
            pos_tags = nltk.pos_tag(tokens)

            i = 0
            while i < len(pos_tags):
                if pos_tags[i][1] == 'NNP':
                    # Skip if this word is an action verb
                    if pos_tags[i][0].lower() in action_verbs_lower:
                        i += 1
                        continue

                    name_parts = [pos_tags[i][0]]
                    j = i + 1
                    while j < len(pos_tags) and pos_tags[j][1] == 'NNP':
                        # Skip action verbs even in the middle of proper noun sequences
                        if pos_tags[j][0].lower() not in action_verbs_lower:
                            name_parts.append(pos_tags[j][0])
                        j += 1

                    # Only add if we have actual name parts (not just action verbs)
                    if len(name_parts) >= 1:
                        all_names.append(' '.join(name_parts))
                    i = j
                else:
                    i += 1
        except Exception as e:
            logger.debug(f"POS extraction failed: {e}")

        # Method 3: Regex Pattern Matching
        patterns = [
            r'\b[A-Z][a-z]+\s+[A-Z][a-z]+\b',  # Standard Firstname Lastname
            r'\b[A-Z][a-z]+\b',  # Single capitalized name
        ]

        for pattern in patterns:
            matches = re.findall(pattern, text)
            # Filter out action verbs from regex matches
            for match in matches:
                words = match.split()
                # Only add if no word in the match is an action verb
                if not any(word.lower() in action_verbs_lower for word in words):
                    all_names.append(match)

        
        # Filter out common false positives (action verbs and system words)
        false_positives = {
            # Action verbs
            'Change', 'Remove', 'Update', 'Delete', 'Add', 'Create',
            'Show', 'Find', 'Get', 'View', 'Display', 'List',
            'Fetch', 'Retrieve', 'Pull', 'Access', 'Lookup', 'Search', 'Locate', 'Bring',
            'Mark', 'Set', 'Record', 'Edit', 'Modify', 'Alter', 'Revise',
            'Erase', 'Drop', 'Register', 'Enroll',
        }
        filtered_names = []

        for name in all_names:
            if name not in filtered_names and not any(fp in name for fp in false_positives):
                filtered_names.append(name)

        # Use voting/frequency to determine most likely name
        if filtered_names:
            name_counts = Counter(filtered_names)
            most_common = name_counts.most_common(1)[0][0]
            confidence = name_counts[most_common] / len(all_names) if all_names else 0
            return most_common, confidence, filtered_names

        return None, 0.0, []
New contributor
saul ye is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
1
  • 3
    Before you get too bogged down in your particular approach, read this for a wider perspective. Commented yesterday

1 Answer 1

2

I've run into this exact issue before! Trying to split names with regex or basic NLTK chunks is a nightmare because there are just too many edge cases (like middle names, titles, or just two capitalized words next to each other). Honestly, the best way to handle this without pulling your hair out is to use spaCy. It has a pre-trained model for Named Entity Recognition (NER) that's much smarter than standard regex because it looks at the context of the sentence, not just the capitalization. It handles compound names like "John Michael Smith" perfectly out of the box. Here's how you can set it up: First, just grab the library and the small English model:

pip install spacy
python -m spacy download en_core_web_sm

Then you can replace your complex logic with this:

import spacy
def extract_names(text):
    # Load the model (you only need to do this once)
    nlp = spacy.load("en_core_web_sm")
    doc = nlp(text)
    names = []
    for ent in doc.ents:
        # We only care about entities labeled as 'PERSON'
        if ent.label_ == "PERSON":
            names.append(ent.text)
    return names
# It handles the compound name correctly:
print(extract_names("John Michael Smith went to the store."))
# Output: ['John Michael Smith']

This should save you a lot of headache with the false positives you were seeing with the action verbs, too, since spaCy knows "Update" at the start of a sentence usually isn't a person! Hope that helps.

New contributor
Umang Rajput is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.