#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, как описано здесь . Я обновил ответ другим подходом, который может оказаться более полезным. Дайте мне знать, если это сработает для вас.