Сопоставление строки Python с фреймом данных Spark

#python #apache-spark #string-matching #string-comparison #fuzzywuzzy

#python #apache-искра #сопоставление строк #сравнение строк #fuzzywuzzy

Вопрос:

У меня есть искровой фрейм данных

 id  | city  | fruit | quantity
-------------------------
0   |  CA   | apple | 300
1   |  CA   | appel | 100
2   |  CA   | orange| 20
3   |  CA   | berry | 10
 

Я хочу получить строки, где фрукты apple или orange . Поэтому я использую Spark SQL:

 SELECT * FROM table WHERE fruit LIKE '%apple%' OR fruit LIKE '%orange%';
 

Он возвращается

 id  | city  | fruit | quantity
-------------------------
0   |  CA   | apple | 300
2   |  CA   | orange| 20
 

Но предполагается, что он возвращает

 id  | city  | fruit | quantity
-------------------------
0   |  CA   | apple | 300
1   |  CA   | appel | 100
2   |  CA   | orange| 20
 

поскольку строка 1 — это просто орфографическая ошибка.

Поэтому я планирую использовать fuzzywuzzy для сопоставления строк. Я знаю, что

 import fuzzywuzzy
from fuzzywuzzy import fuzz
from fuzzywuzzy import process

print(fuzz.partial_ratio('apple', 'apple')) -> 100
print(fuzz.partial_ratio('apple', 'appel')) -> 83
 

Но я не уверен, как применить это к столбцу в dataframe, чтобы получить соответствующие строки

Комментарии:

1. Вы можете зарегистрировать функцию fuzzywuzzy как udf.

Ответ №1:

Поскольку вы заинтересованы в реализации нечеткого сопоставления в качестве фильтра, вы должны сначала определить порог того, насколько похожи вы хотели бы, чтобы совпадения.

Подход 1

Для вашего fuzzywuzzy импорта это может быть 80 для целей этой демонстрации (настройте в зависимости от ваших потребностей). Затем вы могли бы реализовать udf для применения импортированного кода нечеткой логики, например

 from pyspark.sql import functions as F
from pyspark.sql import types as T

F.udf(T.BooleanType())
def is_fuzzy_match(field_value,search_value, threshold=80):
    from fuzzywuzzy import fuzz
    return fuzz.partial_ratio(field_value, search_value) > threshold
 

Затем примените ваш udf в качестве фильтра к вашему фрейму данных

 df = (
    df.where(
          is_fuzzy_match(F.col("fruit"),F.lit("apple"))  | 
          is_fuzzy_match(F.col("fruit"),F.lit("orange"))  
    )
)
 

Подход 2: рекомендуется

Однако udfs может быть дорогостоящим при выполнении в spark, и spark уже реализовал levenshtein функцию, которая также полезна здесь. Вы можете начать читать больше о том, как расстояние Левенштейна обеспечивает нечеткое сопоставление .

При таком подходе ваш код кода выглядит так, как будто используется пороговое значение 3 ниже

 from pyspark.sql import functions as F

df = df.where(
    (
        F.levenshtein(
            F.col("fruit"),
            F.lit("apple")
        ) < 3
     ) |
     (
        F.levenshtein(
            F.col("fruit"),
            F.lit("orange")
        ) < 3
     ) 
)

df.show()
 
  --- ---- ------ -------- 
| id|city| fruit|quantity|
 --- ---- ------ -------- 
|  0|  CA| apple|     300|
|  1|  CA| appel|     100|
|  2|  CA|orange|      20|
 --- ---- ------ -------- 
 

Для целей отладки результат Левенштейна был включен ниже

 df.withColumn("diff",
    F.levenshtein(
        F.col("fruit"),
        F.lit("apple")
    )
).show()
 
  --- ---- ------ -------- ---- 
| id|city| fruit|quantity|diff|
 --- ---- ------ -------- ---- 
|  0|  CA| apple|     300|   0|
|  1|  CA| appel|     100|   2|
|  2|  CA|orange|      20|   5|
|  3|  CA| berry|      10|   5|
 --- ---- ------ -------- ---- 
 

Обновление 1

В ответ на дополнительные примеры данных, предоставленные Op в комментариях:

Если у меня есть фрукт, такой как кашмирское яблоко, и я хочу, чтобы он соответствовал apple

Подход 3

Вы можете попробовать следующий подход и настроить пороговое значение по желанию.

Поскольку вы заинтересованы в сопоставлении возможности использования фруктов с ошибками по всему тексту, вы можете попытаться применить Левенштейна к каждому фрагменту в названии фрукта. Приведенные ниже функции (не udfs, но для удобства чтения упрощают применение задачи) реализуют этот подход. matches_fruit_ratio пытается определить, какая часть совпадения найдена, в то время matches_fruit как определяет максимум matches_fruit_ratio для каждого фрагмента имени фрукта, разделенного пробелом.

 
from pyspark.sql import functions as F

def matches_fruit_ratio(fruit_column,fruit_search,threshold=0.3):
    return (F.length(fruit_column) - F.levenshtein(
        fruit_column,
        F.lit(fruit_search)
    )) / F.length(fruit_column) 

def matches_fruit(fruit_column,fruit_search,threshold=0.6):
    return F.array_max(F.transform(
        F.split(fruit_column," "),
        lambda fruit_piece : matches_fruit_ratio(fruit_piece,fruit_search)
    )) >= threshold

 

Это можно использовать следующим образом:

 df = df.where(
    
    matches_fruit(
        F.col("fruit"),
        "apple"
    ) | matches_fruit(
        F.col("fruit"),
        "orange"
    )
)
df.show()
 
  --- ---- ------------- -------- 
| id|city|        fruit|quantity|
 --- ---- ------------- -------- 
|  0|  CA|        apple|     300|
|  1|  CA|        appel|     100|
|  2|  CA|       orange|      20|
|  4|  CA|  apply berry|       3|
|  5|  CA|  apple berry|       1|
|  6|  CA|kashmir apple|       5|
|  7|  CA|kashmir appel|       8|
 --- ---- ------------- -------- 
 

В целях отладки я добавил дополнительные образцы данных и выходные столбцы для различных компонентов каждой функции, демонстрируя, как можно использовать эту функцию

 df.withColumn("length",
    F.length(
        "fruit"
    )
).withColumn("levenshtein",
    F.levenshtein(
        F.col("fruit"),
        F.lit("apple")
    )
).withColumn("length - levenshtein",
    F.length(
        "fruit"
    ) - F.levenshtein(
        F.col("fruit"),
        F.lit("apple")
    )
).withColumn(
    "matches_fruit_ratio",
    matches_fruit_ratio(
        F.col("fruit"),
        "apple"
    )
).withColumn(
    "matches_fruit_values_before_threshold",
    F.array_max(F.transform(
        F.split("fruit"," "),
        lambda fruit_piece : matches_fruit_ratio(fruit_piece,"apple")
    ))
).withColumn(
    "matches_fruit",
    matches_fruit(
        F.col("fruit"),
        "apple"
    )
).show()
 
  --- ---- ------------- -------- ------ ----------- -------------------- ------------------- ------------------------------------- ------------- 
| id|city|        fruit|quantity|length|levenshtein|length - levenshtein|matches_fruit_ratio|matches_fruit_values_before_threshold|matches_fruit|
 --- ---- ------------- -------- ------ ----------- -------------------- ------------------- ------------------------------------- ------------- 
|  0|  CA|        apple|     300|     5|          0|                   5|                1.0|                                  1.0|         true|
|  1|  CA|        appel|     100|     5|          2|                   3|                0.6|                                  0.6|         true|
|  2|  CA|       orange|      20|     6|          5|                   1|0.16666666666666666|                  0.16666666666666666|        false|
|  3|  CA|        berry|      10|     5|          5|                   0|                0.0|                                  0.0|        false|
|  4|  CA|  apply berry|       3|    11|          6|                   5|0.45454545454545453|                                  0.8|         true|
|  5|  CA|  apple berry|       1|    11|          6|                   5|0.45454545454545453|                                  1.0|         true|
|  6|  CA|kashmir apple|       5|    13|          8|                   5|0.38461538461538464|                                  1.0|         true|
|  7|  CA|kashmir appel|       8|    13|         10|                   3|0.23076923076923078|                                  0.6|         true|
 --- ---- ------------- -------- ------ ----------- -------------------- ------------------- ------------------------------------- ------------- 
 

Комментарии:

1. Если у меня есть такой фрукт, как кашмирское яблоко, и я хочу, чтобы он сочетался с яблоком. Если я использую расстояние Левенштейна, оно будет показывать расстояние как 8, вот почему я хочу использовать fuzz.partial_ratio

2. @JohnConstantine Спасибо, что поделились дополнительным примером, который дает лучшее представление о вашем варианте использования. Вы можете попробовать udf, упомянутый в ответе, или реализовать partial_ratio в spark, как описано здесь . Я обновил ответ другим подходом, который может оказаться более полезным. Дайте мне знать, если это сработает для вас.