Comparaison des chaînes de similarité en Java


111

Je veux comparer plusieurs chaînes entre elles et trouver celles qui sont les plus similaires. Je me demandais s'il existe une bibliothèque, une méthode ou une meilleure pratique qui me renverrait quelles chaînes sont plus similaires à d'autres chaînes. Par exemple:

  • "Le renard rapide a sauté" -> "Le renard a sauté"
  • "Le renard rapide a sauté" -> "Le renard"

Cette comparaison renverrait que le premier est plus similaire que le second.

Je suppose que j'ai besoin d'une méthode telle que:

double similarityIndex(String s1, String s2)

Y a-t-il une telle chose quelque part?

EDIT: Pourquoi est-ce que je fais ça? J'écris un script qui compare la sortie d'un fichier MS Project à la sortie d'un système hérité qui gère les tâches. Étant donné que l'ancien système a une largeur de champ très limitée, lorsque les valeurs sont ajoutées, les descriptions sont abrégées. Je veux un moyen semi-automatisé pour trouver quelles entrées de MS Project sont similaires aux entrées du système afin que je puisse obtenir les clés générées. Il présente des inconvénients, car il doit encore être vérifié manuellement, mais cela économiserait beaucoup de travail

Réponses:


82

Oui, il existe de nombreux algorithmes bien documentés comme:

  • Similitude cosinus
  • Similitude Jaccard
  • Coefficient de dés
  • Similitude correspondante
  • Chevauchement de similarité
  • etc

Un bon résumé ("Sam's String Metrics") peut être trouvé ici (lien original mort, donc il renvoie à Internet Archive)

Vérifiez également ces projets:


18
+1 Le site simmetrics ne semble plus actif. Cependant, j'ai trouvé le code sur sourceforge: sourceforge.net/projects/simmetrics Merci pour le pointeur.
Michael Merchant

7
Le lien "vous pouvez vérifier" est rompu.
Kiril

1
C'est pourquoi Michael Merchant a publié le lien correct ci-dessus.
emilyk

2
Le pot pour simmetrics sur sourceforge est un peu obsolète, github.com/mpkorstanje/simmetrics est la page github mise à jour avec des artefacts
maven

Pour ajouter au commentaire de @MichaelMerchant, le projet est également disponible sur github . Pas très actif non plus mais un peu plus récent que sourceforge.
Ghurdyl

163

La manière courante de calculer la similitude entre deux chaînes de 0% à 100% , telle qu'elle est utilisée dans de nombreuses bibliothèques, est de mesurer combien (en%) il faudrait changer la chaîne la plus longue pour la transformer en la plus courte:

/**
 * Calculates the similarity (a number within 0 and 1) between two strings.
 */
public static double similarity(String s1, String s2) {
  String longer = s1, shorter = s2;
  if (s1.length() < s2.length()) { // longer should always have greater length
    longer = s2; shorter = s1;
  }
  int longerLength = longer.length();
  if (longerLength == 0) { return 1.0; /* both strings are zero length */ }
  return (longerLength - editDistance(longer, shorter)) / (double) longerLength;
}
// you can use StringUtils.getLevenshteinDistance() as the editDistance() function
// full copy-paste working code is below


Calcul du editDistance():

La editDistance()fonction ci-dessus devrait calculer la distance d'édition entre les deux chaînes. Il existe plusieurs implémentations à cette étape, chacune pouvant mieux convenir à un scénario spécifique. Le plus courant est l' algorithme de distance de Levenshtein et nous l'utiliserons dans notre exemple ci-dessous (pour les très grandes chaînes, d'autres algorithmes sont susceptibles de mieux fonctionner).

Voici deux options pour calculer la distance d'édition:


Exemple de travail:

Voir la démo en ligne ici.

public class StringSimilarity {

  /**
   * Calculates the similarity (a number within 0 and 1) between two strings.
   */
  public static double similarity(String s1, String s2) {
    String longer = s1, shorter = s2;
    if (s1.length() < s2.length()) { // longer should always have greater length
      longer = s2; shorter = s1;
    }
    int longerLength = longer.length();
    if (longerLength == 0) { return 1.0; /* both strings are zero length */ }
    /* // If you have Apache Commons Text, you can use it to calculate the edit distance:
    LevenshteinDistance levenshteinDistance = new LevenshteinDistance();
    return (longerLength - levenshteinDistance.apply(longer, shorter)) / (double) longerLength; */
    return (longerLength - editDistance(longer, shorter)) / (double) longerLength;

  }

  // Example implementation of the Levenshtein Edit Distance
  // See http://rosettacode.org/wiki/Levenshtein_distance#Java
  public static int editDistance(String s1, String s2) {
    s1 = s1.toLowerCase();
    s2 = s2.toLowerCase();

    int[] costs = new int[s2.length() + 1];
    for (int i = 0; i <= s1.length(); i++) {
      int lastValue = i;
      for (int j = 0; j <= s2.length(); j++) {
        if (i == 0)
          costs[j] = j;
        else {
          if (j > 0) {
            int newValue = costs[j - 1];
            if (s1.charAt(i - 1) != s2.charAt(j - 1))
              newValue = Math.min(Math.min(newValue, lastValue),
                  costs[j]) + 1;
            costs[j - 1] = lastValue;
            lastValue = newValue;
          }
        }
      }
      if (i > 0)
        costs[s2.length()] = lastValue;
    }
    return costs[s2.length()];
  }

  public static void printSimilarity(String s, String t) {
    System.out.println(String.format(
      "%.3f is the similarity between \"%s\" and \"%s\"", similarity(s, t), s, t));
  }

  public static void main(String[] args) {
    printSimilarity("", "");
    printSimilarity("1234567890", "1");
    printSimilarity("1234567890", "123");
    printSimilarity("1234567890", "1234567");
    printSimilarity("1234567890", "1234567890");
    printSimilarity("1234567890", "1234567980");
    printSimilarity("47/2010", "472010");
    printSimilarity("47/2010", "472011");
    printSimilarity("47/2010", "AB.CDEF");
    printSimilarity("47/2010", "4B.CDEFG");
    printSimilarity("47/2010", "AB.CDEFG");
    printSimilarity("The quick fox jumped", "The fox jumped");
    printSimilarity("The quick fox jumped", "The fox");
    printSimilarity("kitten", "sitting");
  }

}

Production:

1.000 is the similarity between "" and ""
0.100 is the similarity between "1234567890" and "1"
0.300 is the similarity between "1234567890" and "123"
0.700 is the similarity between "1234567890" and "1234567"
1.000 is the similarity between "1234567890" and "1234567890"
0.800 is the similarity between "1234567890" and "1234567980"
0.857 is the similarity between "47/2010" and "472010"
0.714 is the similarity between "47/2010" and "472011"
0.000 is the similarity between "47/2010" and "AB.CDEF"
0.125 is the similarity between "47/2010" and "4B.CDEFG"
0.000 is the similarity between "47/2010" and "AB.CDEFG"
0.700 is the similarity between "The quick fox jumped" and "The fox jumped"
0.350 is the similarity between "The quick fox jumped" and "The fox"
0.571 is the similarity between "kitten" and "sitting"

11
La méthode de distance de Levenshtein est disponible en org.apache.commons.lang3.StringUtils.
Cleankod

@Cleankod Maintenant, il fait partie de commons-text: commons.apache.org/proper/commons-text/javadocs/api-release/org/...
Luiz

15

J'ai traduit l' algorithme de distance de Levenshtein en JavaScript:

String.prototype.LevenshteinDistance = function (s2) {
    var array = new Array(this.length + 1);
    for (var i = 0; i < this.length + 1; i++)
        array[i] = new Array(s2.length + 1);

    for (var i = 0; i < this.length + 1; i++)
        array[i][0] = i;
    for (var j = 0; j < s2.length + 1; j++)
        array[0][j] = j;

    for (var i = 1; i < this.length + 1; i++) {
        for (var j = 1; j < s2.length + 1; j++) {
            if (this[i - 1] == s2[j - 1]) array[i][j] = array[i - 1][j - 1];
            else {
                array[i][j] = Math.min(array[i][j - 1] + 1, array[i - 1][j] + 1);
                array[i][j] = Math.min(array[i][j], array[i - 1][j - 1] + 1);
            }
        }
    }
    return array[this.length][s2.length];
};

11

Vous pouvez utiliser la distance de Levenshtein pour calculer la différence entre deux chaînes. http://en.wikipedia.org/wiki/Levenshtein_distance


2
Levenshtein est idéal pour quelques chaînes, mais ne s'adapte pas aux comparaisons entre un grand nombre de chaînes.
dépenser le

J'ai utilisé Levenshtein en Java avec un certain succès. Je n'ai pas fait de comparaisons sur d'énormes listes, il peut donc y avoir un impact négatif sur les performances. De plus, c'est un peu simple et pourrait utiliser quelques ajustements pour augmenter le seuil des mots plus courts (comme 3 ou 4 caractères) qui ont tendance à être considérés comme plus similaires que le devrait (il n'y a que 3 modifications de chat à chien). suggérés ci-dessous sont à peu près la même chose - Levenshtein est une implémentation particulière des distances d'édition.
Rhubarb

Voici un article montrant comment combiner Levenshtein avec une requête SQL efficace: literatejava.com/sql/fuzzy-string-search-sql
Thomas W

10

Il existe en effet de nombreuses mesures de similarité de chaînes:

  • Distance d'édition de Levenshtein;
  • Distance Damerau-Levenshtein;
  • Similitude Jaro-Winkler;
  • Distance d'édition la plus longue de la sous-séquence commune;
  • Q-Gram (Ukkonen);
  • distance n-gramme (Kondrak);
  • Index Jaccard;
  • Coefficient Sorensen-Dice;
  • Similitude cosinus;
  • ...

Vous pouvez trouver une explication et une implémentation java de ceux-ci ici: https://github.com/tdebatty/java-string-similarity





3

Cela me semble être un outil de recherche de plagiat si votre chaîne se transforme en document. Peut-être qu'une recherche avec ce terme donnera quelque chose de bien.

"Programming Collective Intelligence" comprend un chapitre sur la question de savoir si deux documents sont similaires. Le code est en Python, mais il est propre et facile à porter.


3

Merci au premier répondant, je pense qu'il y a 2 calculs de computeEditDistance (s1, s2). En raison du temps passé, a décidé d'améliorer les performances du code. Alors:

public class LevenshteinDistance {

public static int computeEditDistance(String s1, String s2) {
    s1 = s1.toLowerCase();
    s2 = s2.toLowerCase();

    int[] costs = new int[s2.length() + 1];
    for (int i = 0; i <= s1.length(); i++) {
        int lastValue = i;
        for (int j = 0; j <= s2.length(); j++) {
            if (i == 0) {
                costs[j] = j;
            } else {
                if (j > 0) {
                    int newValue = costs[j - 1];
                    if (s1.charAt(i - 1) != s2.charAt(j - 1)) {
                        newValue = Math.min(Math.min(newValue, lastValue),
                                costs[j]) + 1;
                    }
                    costs[j - 1] = lastValue;
                    lastValue = newValue;
                }
            }
        }
        if (i > 0) {
            costs[s2.length()] = lastValue;
        }
    }
    return costs[s2.length()];
}

public static void printDistance(String s1, String s2) {
    double similarityOfStrings = 0.0;
    int editDistance = 0;
    if (s1.length() < s2.length()) { // s1 should always be bigger
        String swap = s1;
        s1 = s2;
        s2 = swap;
    }
    int bigLen = s1.length();
    editDistance = computeEditDistance(s1, s2);
    if (bigLen == 0) {
        similarityOfStrings = 1.0; /* both strings are zero length */
    } else {
        similarityOfStrings = (bigLen - editDistance) / (double) bigLen;
    }
    //////////////////////////
    //System.out.println(s1 + "-->" + s2 + ": " +
      //      editDistance + " (" + similarityOfStrings + ")");
    System.out.println(editDistance + " (" + similarityOfStrings + ")");
}

public static void main(String[] args) {
    printDistance("", "");
    printDistance("1234567890", "1");
    printDistance("1234567890", "12");
    printDistance("1234567890", "123");
    printDistance("1234567890", "1234");
    printDistance("1234567890", "12345");
    printDistance("1234567890", "123456");
    printDistance("1234567890", "1234567");
    printDistance("1234567890", "12345678");
    printDistance("1234567890", "123456789");
    printDistance("1234567890", "1234567890");
    printDistance("1234567890", "1234567980");

    printDistance("47/2010", "472010");
    printDistance("47/2010", "472011");

    printDistance("47/2010", "AB.CDEF");
    printDistance("47/2010", "4B.CDEFG");
    printDistance("47/2010", "AB.CDEFG");

    printDistance("The quick fox jumped", "The fox jumped");
    printDistance("The quick fox jumped", "The fox");
    printDistance("The quick fox jumped",
            "The quick fox jumped off the balcany");
    printDistance("kitten", "sitting");
    printDistance("rosettacode", "raisethysword");
    printDistance(new StringBuilder("rosettacode").reverse().toString(),
            new StringBuilder("raisethysword").reverse().toString());
    for (int i = 1; i < args.length; i += 2) {
        printDistance(args[i - 1], args[i]);
    }


 }
}

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.