In [1]:
# pip install rapidfuzz scikit-learn numpy

import numpy as np
from collections import Counter
import string
from rapidfuzz import fuzz, distance

test_pairs = [
    # Direct copies and near-copies
    ("The cat sat on the mat.", "The cat sat on the mat."),           # Exact copy
    ("The cat sat on the mat.", "The cat sat on the mat"),            # No punctuation
    ("The cat sat on the mat.", "The  cat  sat  on  the  mat."),      # Extra spaces
    
    # Paraphrases with same meaning
    ("The cat sat on the mat.", "On the mat, the cat was sitting."),  # Structural change
    ("The cat sat on the mat.", "The feline rested on the rug."),     # Synonym replacement
    ("The quick brown fox jumps.", "A fast brown fox leaps."),        # Partial synonym
    
    # Different sentences
    ("The cat sat on the mat.", "The dog ran in the park."),          # Different content
    ("I love programming.", "She enjoys reading books."),             # Completely different
    ("The weather is nice today.", "It's raining outside."),          # Opposite meaning
    
    # Edge cases
    ("Short.", "Short."),                                             # Very short
    ("A B C D E F G", "A B C D E F G"),                              # Repeated words
    ("", ""),                                                         # Empty strings
]
print("done")
done

Jaccard Similarity¶

In [58]:
def jaccard_similarity(sent1, sent2):
    # make lowercase and split into words
    words1 = set(sent1.lower().split())
    words2 = set(sent2.lower().split())
    intersection = words1.intersection(words2)
    union = words1.union(words2)
    return float(len(intersection)) / len(union) if union else 0.0

small_test_pairs = [
    ("The cat sat on the mat.", "The cat sat on the mat."),     # Copy
    ("The cat sat on the mat.", "On the mat sat the cat."),     # Same words rearranged
    ("The cat sat on the mat.", "The dog ran in the park")      # Different
]

print(f"{'Sentence 1':<41} {'Sentence 2:':<40} {'Similarity Score:'}")
print("=" * 100)

for sent1, sent2 in test_pairs:
    similarity = jaccard_similarity(sent1, sent2)
    print(f"{sent1:<30} {'vs':<10} {sent2:<40} {similarity:.3f}") # 3 decimal places
    #print("-"* 50)
Sentence 1                                Sentence 2:                              Similarity Score:
====================================================================================================
The cat sat on the mat.        vs         The cat sat on the mat.                  1.000
The cat sat on the mat.        vs         The cat sat on the mat                   0.667
The cat sat on the mat.        vs         The  cat  sat  on  the  mat.             1.000
The cat sat on the mat.        vs         On the mat, the cat was sitting.         0.375
The cat sat on the mat.        vs         The feline rested on the rug.            0.250
The quick brown fox jumps.     vs         A fast brown fox leaps.                  0.250
The cat sat on the mat.        vs         The dog ran in the park.                 0.111
I love programming.            vs         She enjoys reading books.                0.000
The weather is nice today.     vs         It's raining outside.                    0.000
Short.                         vs         Short.                                   1.000
A B C D E F G                  vs         A B C D E F G                            1.000
                               vs                                                  0.000

--- Leneshtein Similarity ---¶

Character & Word

In [57]:
def char_levenshtein_similarity(sent1, sent2):
    """ Character based edit-distance similarity """
    if not sent1 and not sent2:
        return 1.0
    if not sent1 or not sent2:
        return 0.0
    
    max_len = max(len(sent1), len(sent2))
    edit_distance = distance.Levenshtein.distance(sent1, sent2)
    return 1 - (edit_distance / max_len)

def word_levenshtein_similarity(sent1, sent2):
    """ Word based edit-distance similarity """
    words1 = sent1.lower().split()
    words2 = sent2.lower().split()
    
    if not words1 and not words2:
        return 1.0
    if not words1 or not words2:
        return 0.0
    
    max_len = max(len(words1), len(words2))
    edit_distance = distance.Levenshtein.distance(words1, words2)
    return 1 - (edit_distance / max_len)

    

print(f"{'Sentence 1':<41} {'Sentence 2:':<20} {'Similarities ->':<19} {'Char:':<10} {'Word:'}")
print("=" * 100)

for sent1, sent2 in test_pairs:
    char_similarity = char_levenshtein_similarity(sent1, sent2)
    word_similarity = word_levenshtein_similarity(sent1, sent2)
    print(f"{sent1:<30} {'vs':<10} {sent2:<40} {char_similarity:.3f}{'':<5} {word_similarity:.3F} ") # 3 decimal places
    #print("-"* 50
Sentence 1                                Sentence 2:          Similarities ->     Char:      Word:
====================================================================================================
The cat sat on the mat.        vs         The cat sat on the mat.                  1.000      1.000 
The cat sat on the mat.        vs         The cat sat on the mat                   0.957      0.833 
The cat sat on the mat.        vs         The  cat  sat  on  the  mat.             0.821      1.000 
The cat sat on the mat.        vs         On the mat, the cat was sitting.         0.344      0.143 
The cat sat on the mat.        vs         The feline rested on the rug.            0.517      0.500 
The quick brown fox jumps.     vs         A fast brown fox leaps.                  0.577      0.400 
The cat sat on the mat.        vs         The dog ran in the park.                 0.625      0.333 
I love programming.            vs         She enjoys reading books.                0.200      0.000 
The weather is nice today.     vs         It's raining outside.                    0.192      0.000 
Short.                         vs         Short.                                   1.000      1.000 
A B C D E F G                  vs         A B C D E F G                            1.000      1.000 
                               vs                                                  1.000      1.000 

--- Cosine Similarity ---¶

In [38]:
def cosine_similarity_bow(sent1, sent2):
    """ dosine similarity using bag-of-words """
    words1 = sent1.lower().translate(str.maketrans('', '', string.punctuation)).split()
    words2 = sent2.lower().translate(str.maketrans('', '', string.punctuation)).split()
    
    if not words1 and not words2:
        return 1.0
    
    vocabulary = set(words1 + words2)
    if not vocabulary:
        return 0.0
    
    # Create frequency vectors
    freq1 = Counter(words1)
    freq2 = Counter(words2)
    
    # Convert to vectors
    vec1 = np.array([freq1[word] for word in vocabulary])
    vec2 = np.array([freq2[word] for word in vocabulary])
    
    # Compute cosine similarity
    dot_product = np.dot(vec1, vec2)
    norm1 = np.linalg.norm(vec1)
    norm2 = np.linalg.norm(vec2)
    
    if norm1 == 0 or norm2 == 0:
        return 0.0
    
    return dot_product / (norm1 * norm2)

print(f"{'Sentence 1':<41} {'Sentence 2:':<40} {'Similarity Score:'}")
print("=" * 100)

for sent1, sent2 in test_pairs:
    similarity = cosine_similarity_bow(sent1, sent2)
    print(f"{sent1:<30} {'vs':<10} {sent2:<40} {similarity:.3f}") # 3 decimal places
    #print("-"* 50)
Sentence 1                                Sentence 2:                              Similarity Score:
====================================================================================================
The cat sat on the mat.        vs         The cat sat on the mat.                  1.000
The cat sat on the mat.        vs         The cat sat on the mat                   1.000
The cat sat on the mat.        vs         The  cat  sat  on  the  mat.             1.000
The cat sat on the mat.        vs         On the mat, the cat was sitting.         0.825
The cat sat on the mat.        vs         The feline rested on the rug.            0.625
The quick brown fox jumps.     vs         A fast brown fox leaps.                  0.400
The cat sat on the mat.        vs         The dog ran in the park.                 0.500
I love programming.            vs         She enjoys reading books.                0.000
The weather is nice today.     vs         It's raining outside.                    0.000
Short.                         vs         Short.                                   1.000
A B C D E F G                  vs         A B C D E F G                            1.000
                               vs                                                  1.000

--- Fuzzy ratios ---¶

In [5]:
def fuzzy_ratio_similarity(sent1, sent2):
    """Fuzzy string matching ratio"""
    return fuzz.ratio(sent1.lower(), sent2.lower()) / 100.0

def fuzzy_partial_ratio(sent1, sent2):
    """Fuzzy partial string matching"""
    return fuzz.partial_ratio(sent1.lower(), sent2.lower()) / 100.0

def fuzzy_token_sort_ratio(sent1, sent2):
    """Fuzzy token sorting ratio (ignore order)"""
    return fuzz.token_sort_ratio(sent1.lower(), sent2.lower()) / 100.0

def fuzzy_token_set_ratio(sent1, sent2):
    """ Fuzzy token set ratio (duplicates) """
    return fuzz.token_set_ratio(sent1.lower(), sent2.lower()) / 100.0

print("Fuzzy ratio Examples")
print("=" * 90)
print(f"{'Case':<30} {'Ratio':<8} {'Partial':<8} {'Token Sort':<12} {'Token Set':<8} Description")
print("=" * 90)

for sent1, sent2 in test_pairs:
        ratio = int(fuzz.ratio(sent1, sent2))
        partial = int(fuzz.partial_ratio(sent1, sent2)) 
        token_sort = int(fuzz.token_sort_ratio(sent1, sent2))
        token_set = int(fuzz.token_set_ratio(sent1, sent2))
        
        # description
        if sent1 == sent2:
            desc = "Exact copy"
        elif sorted(sent1.split()) == sorted(sent2.split()):
            desc = "Same words, different order"
        elif set(sent1.split()).issubset(set(sent2.split())) or set(sent2.split()).issubset(set(sent1.split())):
            desc = "Subset relationship"
        else:
            desc = "Different content"
            
        print(f"{sent1[:28]:<30} {ratio:<8} {partial:<8} {token_sort:<12} {token_set:<8} {desc}")
Fuzzy ratio Examples
==========================================================================================
Case                           Ratio    Partial  Token Sort   Token Set Description
==========================================================================================
The cat sat on the mat.        100      100      100          100      Exact copy
The cat sat on the mat.        97       100      97           97       Different content
The cat sat on the mat.        90       82       100          100      Same words, different order
The cat sat on the mat.        47       57       58           62       Different content
The cat sat on the mat.        61       60       53           60       Different content
The quick brown fox jumps.     61       70       57           57       Different content
The cat sat on the mat.        63       63       55           55       Different content
I love programming.            40       43       40           40       Different content
The weather is nice today.     38       47       29           29       Different content
Short.                         100      100      100          100      Exact copy
A B C D E F G                  100      100      100          100      Exact copy
                               100      100      100          0        Exact copy

--- Longest common sub-sequence ----¶

In [37]:
def longest_common_subsequence(sent1, sent2):
    """ Longest common subsequence similarity """
    if not sent1 or not sent2:
        return 0.0
    
    # Remove punctuation for better matching
    sent1_clean = sent1.lower().translate(str.maketrans('', '', string.punctuation))
    sent2_clean = sent2.lower().translate(str.maketrans('', '', string.punctuation))
    
    m, n = len(sent1_clean), len(sent2_clean)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if sent1_clean[i-1] == sent2_clean[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    
    lcs_length = dp[m][n]
    return lcs_length / max(m, n) if max(m, n) > 0 else 0.0

print(f"{'Sentence 1':<41} {'Sentence 2:':<40} {'Similarity Score:'}")
print("=" * 100)

for sent1, sent2 in test_pairs:
    similarity = longest_common_subsequence(sent1, sent2)
    print(f"{sent1:<30} {'vs':<10} {sent2:<40} {similarity:.3f}") # 3 decimal places
    #print("-"* 50)
Sentence 1                                Sentence 2:                              Similarity Score:
====================================================================================================
The cat sat on the mat.        vs         The cat sat on the mat.                  1.000
The cat sat on the mat.        vs         The cat sat on the mat                   1.000
The cat sat on the mat.        vs         The  cat  sat  on  the  mat.             0.815
The cat sat on the mat.        vs         On the mat, the cat was sitting.         0.433
The cat sat on the mat.        vs         The feline rested on the rug.            0.536
The quick brown fox jumps.     vs         A fast brown fox leaps.                  0.560
The cat sat on the mat.        vs         The dog ran in the park.                 0.609
I love programming.            vs         She enjoys reading books.                0.333
The weather is nice today.     vs         It's raining outside.                    0.360
Short.                         vs         Short.                                   1.000
A B C D E F G                  vs         A B C D E F G                            1.000
                               vs                                                  0.000

--- Containment Similarity ---¶

Percentage of Sentance A in Sentance B
containment(A, B) = |words(A) ∩ words(B)| / |words(A)|

In [ ]:
def containment_similarity(sent1, sent2):
    # sent1 in sent2 (asymmetric)
    """ What percentage of sent1's words are in sent2 """
    words1 = set(sent1.lower().translate(str.maketrans('', '', string.punctuation)).split())
    words2 = set(sent2.lower().translate(str.maketrans('', '', string.punctuation)).split())
    
    if not words1:
        return 0.0
    
    common = words1.intersection(words2)
    return len(common) / len(words1)

Evaluate BaseLine Methods¶

In [25]:
def evaluate_baseline_methods(pairs):
    """ Evaluate all baseline methods on test pairs"""
    methods = {
        'Jaccard': jaccard_similarity,
        'Levenshtein (char)': char_levenshtein_similarity,
        'Levenshtein (word)': word_levenshtein_similarity,
        'Cosine BOW': cosine_similarity_bow,
        'Fuzzy Ratio': fuzzy_ratio_similarity,
        'Fuzzy Partial': fuzzy_partial_ratio,
        'Fuzzy Token Sort': fuzzy_token_sort_ratio,
        'Fuzzy Token Set': fuzzy_token_set_ratio,
        'LCS': longest_common_subsequence,
        'Containment': containment_similarity,
    }
    
    results = {method: [] for method in methods}
    
    for sent1, sent2 in pairs:
        for method_name, method_func in methods.items():
            similarity = method_func(sent1, sent2)
            results[method_name].append(similarity)
    
    return results, methods

def print_comparison_table(results, pairs):
    """ Print a formatted comparison table """
    print(f"{'Pair':<30} {'Sentence 1':<30} {'Sentence 2':<30}")
    print("=" * 100)
    
    for i, (sent1, sent2) in enumerate(pairs):
        # if sentence too long
        display_sent1 = sent1[:27] + "..." if len(sent1) > 30 else sent1
        display_sent2 = sent2[:27] + "..." if len(sent2) > 30 else sent2
        
        print(f"{f'Pair {i+1}:':<30} {display_sent1:<30} {display_sent2:<30}")
        
        # Print similarities for this pair
        for method_name in results:
            similarity = results[method_name][i]
            print(f"{'':<30} {method_name + ':':<20} {similarity:.3f}")
        print("-" * 100)

results, methods = evaluate_baseline_methods(test_pairs)
print_comparison_table(results, test_pairs)
Pair                           Sentence 1                     Sentence 2                    
====================================================================================================
Pair 1:                        The cat sat on the mat.        The cat sat on the mat.       
                               Jaccard:             1.000
                               Levenshtein (char):  1.000
                               Levenshtein (word):  1.000
                               Cosine BOW:          1.000
                               Fuzzy Ratio:         1.000
                               Fuzzy Partial:       1.000
                               Fuzzy Token Sort:    1.000
                               Fuzzy Token Set:     1.000
                               LCS:                 1.000
                               Containment:         1.000
----------------------------------------------------------------------------------------------------
Pair 2:                        The cat sat on the mat.        The cat sat on the mat        
                               Jaccard:             0.667
                               Levenshtein (char):  0.957
                               Levenshtein (word):  0.833
                               Cosine BOW:          1.000
                               Fuzzy Ratio:         0.978
                               Fuzzy Partial:       1.000
                               Fuzzy Token Sort:    0.978
                               Fuzzy Token Set:     0.973
                               LCS:                 1.000
                               Containment:         1.000
----------------------------------------------------------------------------------------------------
Pair 3:                        The cat sat on the mat.        The  cat  sat  on  the  mat.  
                               Jaccard:             1.000
                               Levenshtein (char):  0.821
                               Levenshtein (word):  1.000
                               Cosine BOW:          1.000
                               Fuzzy Ratio:         0.902
                               Fuzzy Partial:       0.826
                               Fuzzy Token Sort:    1.000
                               Fuzzy Token Set:     1.000
                               LCS:                 0.815
                               Containment:         1.000
----------------------------------------------------------------------------------------------------
Pair 4:                        The cat sat on the mat.        On the mat, the cat was sit...
                               Jaccard:             0.375
                               Levenshtein (char):  0.344
                               Levenshtein (word):  0.143
                               Cosine BOW:          0.825
                               Fuzzy Ratio:         0.509
                               Fuzzy Partial:       0.619
                               Fuzzy Token Sort:    0.764
                               Fuzzy Token Set:     0.723
                               LCS:                 0.433
                               Containment:         0.800
----------------------------------------------------------------------------------------------------
Pair 5:                        The cat sat on the mat.        The feline rested on the rug. 
                               Jaccard:             0.250
                               Levenshtein (char):  0.517
                               Levenshtein (word):  0.500
                               Cosine BOW:          0.625
                               Fuzzy Ratio:         0.615
                               Fuzzy Partial:       0.605
                               Fuzzy Token Sort:    0.538
                               Fuzzy Token Set:     0.480
                               LCS:                 0.536
                               Containment:         0.400
----------------------------------------------------------------------------------------------------
Pair 6:                        The quick brown fox jumps.     A fast brown fox leaps.       
                               Jaccard:             0.250
                               Levenshtein (char):  0.577
                               Levenshtein (word):  0.400
                               Cosine BOW:          0.400
                               Fuzzy Ratio:         0.612
                               Fuzzy Partial:       0.700
                               Fuzzy Token Sort:    0.531
                               Fuzzy Token Set:     0.562
                               LCS:                 0.560
                               Containment:         0.400
----------------------------------------------------------------------------------------------------
Pair 7:                        The cat sat on the mat.        The dog ran in the park.      
                               Jaccard:             0.111
                               Levenshtein (char):  0.625
                               Levenshtein (word):  0.333
                               Cosine BOW:          0.500
                               Fuzzy Ratio:         0.638
                               Fuzzy Partial:       0.636
                               Fuzzy Token Sort:    0.553
                               Fuzzy Token Set:     0.462
                               LCS:                 0.609
                               Containment:         0.200
----------------------------------------------------------------------------------------------------
Pair 8:                        I love programming.            She enjoys reading books.     
                               Jaccard:             0.000
                               Levenshtein (char):  0.200
                               Levenshtein (word):  0.000
                               Cosine BOW:          0.000
                               Fuzzy Ratio:         0.409
                               Fuzzy Partial:       0.432
                               Fuzzy Token Sort:    0.364
                               Fuzzy Token Set:     0.364
                               LCS:                 0.333
                               Containment:         0.000
----------------------------------------------------------------------------------------------------
Pair 9:                        The weather is nice today.     It's raining outside.         
                               Jaccard:             0.000
                               Levenshtein (char):  0.192
                               Levenshtein (word):  0.000
                               Cosine BOW:          0.000
                               Fuzzy Ratio:         0.426
                               Fuzzy Partial:       0.514
                               Fuzzy Token Sort:    0.340
                               Fuzzy Token Set:     0.340
                               LCS:                 0.360
                               Containment:         0.000
----------------------------------------------------------------------------------------------------
Pair 10:                       Short.                         Short.                        
                               Jaccard:             1.000
                               Levenshtein (char):  1.000
                               Levenshtein (word):  1.000
                               Cosine BOW:          1.000
                               Fuzzy Ratio:         1.000
                               Fuzzy Partial:       1.000
                               Fuzzy Token Sort:    1.000
                               Fuzzy Token Set:     1.000
                               LCS:                 1.000
                               Containment:         1.000
----------------------------------------------------------------------------------------------------
Pair 11:                       A B C D E F G                  A B C D E F G                 
                               Jaccard:             1.000
                               Levenshtein (char):  1.000
                               Levenshtein (word):  1.000
                               Cosine BOW:          1.000
                               Fuzzy Ratio:         1.000
                               Fuzzy Partial:       1.000
                               Fuzzy Token Sort:    1.000
                               Fuzzy Token Set:     1.000
                               LCS:                 1.000
                               Containment:         1.000
----------------------------------------------------------------------------------------------------
Pair 12:                                                                                    
                               Jaccard:             0.000
                               Levenshtein (char):  1.000
                               Levenshtein (word):  1.000
                               Cosine BOW:          1.000
                               Fuzzy Ratio:         1.000
                               Fuzzy Partial:       1.000
                               Fuzzy Token Sort:    1.000
                               Fuzzy Token Set:     0.000
                               LCS:                 0.000
                               Containment:         0.000
----------------------------------------------------------------------------------------------------