Skip to content
19 changes: 16 additions & 3 deletions python/cuml/cuml/feature_extraction/_vectorizers.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,17 @@ def get_char_ngrams(self, ngram_size, str_series, doc_id_sr):
tokens = str_series.str.tokenize(self.delimiter)
del str_series

padding = Series(self.delimiter).repeat(len(tokens))
# tokens keeps the original per-document index (repeated per
# token); reset both to a plain range first so the two str.cat()
# calls below align positionally instead of by that index.
tokens = tokens.reset_index(drop=True)
padding = (
Series(self.delimiter)
.repeat(len(tokens))
.reset_index(drop=True)
)
tokens = tokens.str.cat(padding)
padding = padding.reset_index(drop=True)
tokens = padding.str.cat(tokens)
tokens = tokens.reset_index(drop=True)

ngram_sr = tokens.str.character_ngrams(n=ngram_size)

Expand All @@ -235,6 +241,13 @@ def get_char_ngrams(self, ngram_size, str_series, doc_id_sr):
ngram_count = doc_id_df.groupby("doc_id", sort=True).sum()[
"ngram_count"
]
# A document that tokenizes to zero tokens (e.g. an empty string)
# never appears in the groupby above, so its doc_id is silently
# missing from ngram_count's index instead of being present with
# a count of 0. Reindex onto token_count's full per-document
# index so the two stay aligned for get_ngrams' later
# ngram_count[not_empty_docs] boolean filter.
ngram_count = ngram_count.reindex(token_count.index, fill_value=0)
return ngram_sr, ngram_count, token_count

if ngram_size == 1:
Expand Down
17 changes: 17 additions & 0 deletions python/cuml/tests/test_text_feature_extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,23 @@ def test_vectorizer_get_feature_names_deprecated(cls):
np.testing.assert_array_equal(res, model.get_feature_names_out())


def test_tfidf_vectorizer_char_wb_ngrams():
# Regression test for #8416: get_char_ngrams misaligned padded tokens
# across documents once index alignment relied on the original
# per-document index instead of a reset range index.
vectorizer = TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 6))
tfidf_mat = vectorizer.fit_transform(DOCS_GPU)

ref_vectorizer = SkTfidfVect(analyzer="char_wb", ngram_range=(2, 6))
ref = ref_vectorizer.fit_transform(DOCS)

cp.testing.assert_array_almost_equal(tfidf_mat.todense(), ref.toarray())
assert_array_equal(
vectorizer.get_feature_names_out(),
ref_vectorizer.get_feature_names_out(),
)


# ----------------------------------------------------------------
# HashingVectorizer tests
# ----------------------------------------------------------------
Expand Down
Loading