У меня есть фрагмент кода, который условно применяет дополнительные манипуляции к столбцу ('NaicsDescription'
). Я не уверен, что это лучший/самый чистый способ сделать это в Polars. Хотя кажется, что это работает, и я был бы признателен за некоторые рекомендации.
Идея заключается в том, чтобы...
Это верно?
import polars as pl
from unicodedata import normalize
from lingua import Language, LanguageDetectorBuilder
from nltk.corpus import stopwords
languages = [Language.ENGLISH, Language.FRENCH]
detector = LanguageDetectorBuilder.from_languages(*languages).build()
test = pl.DataFrame(
{
"id": [1, 2, 3, 4],
"NaicsDescription": ['Full service restuarants', 'The Manufacturing of toys and trains', 'POWER GENERATING STATIONS', 'the short term rental of cottages']
}
)
def lang_identifier(text: str):
'''
Language identification of text. This is just a simple wrapper around lingua function.
Parameters
----------
text : str
Input string to identify language for.
Returns
-------
language : str
Returns either 'EN' or 'FR' or None if input text is not of type string.
'''
language = None
if isinstance(text, str):
language = detector.detect_language_of(text)
if language:
language = language.iso_code_639_1.name
return language
en_stopwords = '|'.join(set(w.lower() for w in stopwords.words('english')))
fr_stopwords = '|'.join(set(w.lower() for w in stopwords.words('french')))
bilingual_stopwords = '|'.join(set(w.lower() for w in stopwords.words('english') + stopwords.words('french')))
test = test.with_columns(
pl.col('NaicsDescription')
.str.to_lowercase().alias('NaicsDescription_')
).with_columns(
pl.when(pl.col('NaicsDescription').map_elements(lang_identifier, return_dtype=pl.String) == 'FR')
.then(
pl.col('NaicsDescription_').str.replace_all(r'\b(?:' + fr_stopwords + r')\b', ' ')
)
.when(pl.col('NaicsDescription').map_elements(lang_identifier, return_dtype=pl.String) == 'EN')
.then(
pl.col('NaicsDescription_').str.replace_all(r'\b(?:' + en_stopwords + r')\b', ' ')
)
.otherwise(
pl.col('NaicsDescription_').str.replace_all(r'\b(?:' + bilingual_stopwords + r')\b', ' ')
)
).with_columns(
pl.col('NaicsDescription_').map_elements(lambda x: normalize('NFKD',x)
.encode('ascii', errors='ignore')
.decode('utf-8'), return_dtype=pl.String)
.str.replace_all(r'(?:[^\s\w]|_\d)+', ' ')
.str.replace_all(r'\b(?:\d+|\w{1,2})\b', ' ')
.str.replace_all(r'\s\s+', ' ')
.str.strip_chars()
.replace('', None)
)
изменить: добавлен полный рабочий пример...
Трудно сказать, не видя никаких данных, есть ли лучший способ подойти к тому, что вы делаете, или можно ли оптимизировать какие-либо шаги. В будущем работающий пример значительно облегчит помощь.
Кажется, что каждый шаг зависит от предыдущего. Обычно избегайте map_elements
, если только ваша логика не может быть выражена в полярных выражениях. В этом случае кажется, что вашу функцию lang_identifier
вряд ли удастся выразить как полярное выражение, но в этом невозможно быть уверенным. Ваша нормализация Юникода кажется лучшим подходом и соответствует этому ТАК-ответу.
Я думаю, что более всего этот код можно реорганизовать, чтобы цель стала более ясной, а повторяющиеся операции, такие как определение языка и удаление стоп-слов, были вынесены в функцию или переменную.
Вот моя попытка сделать это. Ничего не должно измениться, просто рефакторинг. Вероятно, он не идеален, поскольку я не смог его запустить.
def remove_stopwords(text: pl.Expr, stopwords: str) -> pl.Expr:
"""Removes supplied stopwords from a string."""
return text.str.replace_all(r"\b(?:" + stopwords + r")\b", " ")
naics_description = pl.col("NaicsDescription")
naics_description_lower = pl.col("NaicsDescription").str.to_lowercase()
identified_language = naics_description.map_elements(
lang_identifier,
return_dtype=pl.String,
)
naics_description_stopwords_removed = (
pl.when(identified_language == "FR")
.then(remove_stopwords(naics_description_lower, fr_stopwords))
.when(identified_language == "EN")
.then(remove_stopwords(naics_description_lower, en_stopwords))
.otherwise(remove_stopwords(naics_description_lower, bilingual_stopwords))
)
test = test.with_columns(
naics_description_stopwords_removed.map_elements(
lambda x: normalize("NFKD", x).encode("ascii", errors = "ignore").decode("utf-8"),
return_dtype=pl.String,
)
.str.replace_all(r"(?:[^\s\w]|_\d)+", " ")
.str.replace_all(r"\b(?:\d+|\w{1,2})\b", " ")
.str.replace_all(r"\s\s+", " ")
.str.strip_chars()
.replace("", None)
)
Одна вещь, которую я подвергаю сомнению и с которой борюсь, — это производительность сопоставления регулярных выражений в строках str.replace_all, а теперь и в функции remove_stopwords. В случае с Pandas я бы заранее скомпилировал регулярное выражение только один раз и передал его в функцию Pandas str.replace. Не уверен, стоит ли мне беспокоиться по этому поводу, поскольку я не до конца осознаю, как Polars могут справиться с этой производительностью под капотом.
использование этого метода работает хорошо, и его легче читать. Я протестировал 100 тысяч записей моего фрейма данных из почти 7 миллионов. Производительность приемлемая, однако когда я добавляю дополнительные Map_elements к функции, которая выполняет лемматизацию (с использованием пространственных моделей), производительность резко падает примерно до 7 минут для ввода 100 КБ. Насколько я понимаю, лемматизация в целом обходится дорого и, возможно, мне придется жить без нее... но хотелось бы обойти Map_elements, если смогу.
Что касается производительности замены регулярных выражений, насколько я понимаю, Polars делает это в Rust, поэтому это будет весьма производительно. Если ваши замены регулярных выражений не зависят от предыдущего, вы можете использовать .str.replace_many
, чтобы выполнить их все одновременно, что, как я полагаю, будет способствовать дальнейшему ускорению. Основным ударом по производительности будет любое использование map_elements
. Это возвращается к Python, и вы теряете распараллеливание. Это на порядки медленнее, чем все, что вы делаете на родных поляках. Делайте как можно больше с помощью полярных выражений, однако я понимаю, что для НЛП это может быть невозможно.
Возможно, стоит посмотреть, поддерживают ли плагины Polars то, что вы пытаетесь сделать. Оставаясь в Polars (или, по крайней мере, в numpy или в других форматах, совместимых со стрелками, таких как DuckDB), ваш код не будет зацикливаться элемент за элементом (что на самом деле и делает map_elements
). Это расширение, кажется, реализует некоторые вещи НЛП.
Кажется вероятным, что различные экземпляры
pl.Expr.map_elements
могут быть записаны в родной полярной форме. Если бы вы предоставили более полный работоспособный пример вместе с ожидаемым результатом, было бы легче ответить.