NLPx

Tales of Data Science

Стандартная библиотека Python — для обработки языка: collections.Counter

Counter

Сегодня я хотел бы рассказать об одном интересном классе стандартной библиотеки языка Python, который может пригодиться при обработке языка. Речь пойдет о классе Counter из библиотеки collections.

Кратко

Counter – очень полезная вещь при частотном анализе текста.

Отдавая на вход список слов (который list), получаем объект класса Counter, очень похожий на словарь (который dictionary).

Импортируем класс

from collections import Counter

Простейший пример:

from collections import Counter

data = ['hasta', 'la', 'vista', 'baby', 'la', 'vista']
#создаём объект класса Counter и передаем ему список в качестве аргумента
cntr = Counter(data)

print cntr
Out: Counter({'vista': 2, 'la': 2, 'hasta': 1, 'baby': 1})

Итак, имелся некий список слов произвольной длины, мы создали объект класса Counter и передали ему на вход в качестве единственного аргумента этот список. На выходе получили словарь (а на самом деле объект класса Counter), где ключ — это слово из списка, а значение — количество раз, когда слово встретилось.

В дальнейшем «объект класса Counter» для простоты будем называть просто «каунтер», и да простит меня Розенталь.

Базовые возможности

Доступ к элементам там такой же, как и у словаря: то есть, обращаясь по ключу — получаем значение. Если вдруг обратимся к ключу, которого по недоразумению нет в каунтере, то на выходе получим 0, а не ошибку, что приятно. Добавление и удаление элементов в каунтере такое же, как в словаре.

from collections import Counter

data = ['hasta', 'la', 'vista', 'baby', 'la', 'vista']
cntr = Counter(data)

print cntr
Out: Counter({'vista': 2, 'la': 2, 'hasta': 1, 'baby': 1})

#обращаемся к существующему элементу
print cntr['hasta']
Out: 1

#обращаемся к несуществующему элементу
print cntr['pasta']
Out: 0

#добавляем новый элемент
cntr['pasta'] = 2
print cntr
Out: Counter({'vista': 2, 'pasta': 2, 'la': 2, 'hasta': 1, 'baby': 1})

#удаляем элемент
del cntr['pasta']
print cntr
Out: Counter({'vista': 2, 'la': 2, 'hasta': 1, 'baby': 1})

Методы

Метод Counter.most_common() дает на выходе наиболее частотные элементы из каунтера, главное — указать сколько именно нужно наиболее частотных элементов. Однако, если очень захотеть, то можно получить и наименее частотные документы.

Не стоит забывать, что на выходе этого метода получается список кортежей, где в каждом кортеже первый элемент — это ключ, а второй — значение.

from collections import Counter

data = ['hasta', 'la', 'vista', 'baby', 'la', 'vista']
cntr = Counter(data)

#выводим на экран два самых частотных элемента
print cntr.most_common(2)
Out: [('vista', 2), ('la', 2)]

#выводим на экран два самых редких элемента
#цифра 2 в [:-2-1:-1] означает число элементов, 
#которое нам нужно
print cntr.most_common()[:-2-1:-1]
Out: [('baby', 1), ('hasta', 1)]

С помощью метода Counter.elements() можно получить список элементов в зависимости от их количества. А применяя метод Counter.values() —  список всех значений:

from collections import Counter

data = ['hasta', 'la', 'vista', 'baby', 'la', 'vista']
cntr = Counter(data)

#выводим список всех ключей
print list(cntr.elements())
Out: ['hasta', 'baby', 'vista', 'vista', 'la', 'la']

#выводим список всех значений
print cntr.values()
Out: [1, 1, 2, 2, 2]

Метод Counter.subtract() выдает разность значений для одинаковых ключей в двух объектах типа Counter. То есть, если у нас есть два каунтера cодинаковыми ключами и разными значениями, то, применяя метод Counter.subtract() мы вычитаем значение ключа одного каунтера из другого, при этом отрицательное значение в итоге — допустимо. А если ключи в каунтерах разные, то при вычитании одного каунтера из другого считаем, что несуществующий ключ на самом деле существует, просто его значение —  0. Я думаю, на примере это выглядит более понятно:

from collections import Counter

#вариант 1
cntr1 = Counter({'hasta': 8})
cntr2 = Counter({'hasta': 5})

#Вычитаем cntr2 из cntr1
cntr1.subtract(cntr2)
print cntr1
Out: Counter({'hasta': 3})

#Вычитаем cntr1 из cntr2
cntr2.subtract(cntr1)
print cntr2
Out: Counter({'hasta': -3})

#Вариант2
cntr1 = Counter({'hasta': 8})
cntr2 = Counter({'vista': 5})

#Вычитаем cntr2 из cntr1
cntr1.subtract(cntr2)
print cntr1
Out: Counter({'hasta': 8, 'vista': -5})

#Вычитаем cntr1 из cntr2
cntr2.subtract(cntr1)
print cntr2
Out: Counter({'vista': 5, 'hasta': -8})

Если нам не нравится наличие отрицательных или нулевых значений после операции вычитания методом Counter.subtract(), то можно их убрать довольно нехитрым способом — прибавления к имеющемуся каунтеру пустого каунтера:

from collections import Counter

#убираем элементы с отрицательными значениями
cntr = Counter({'hasta': 5, 'vista': -8})
cntr += Counter()
print cntr
Out: Counter({'hasta': 5})

Преобразование каунтера

С помощью метода Counter.items() можно преобразовать каунтер в список, а все связки ключ-значение — в кортежи, так что на выходе мы получаем список кортежей, где первым элементом становится бывший ключ, а вторым не менее бывшее значение.

Стандартный метод dict() действует ожидаемо — каунтер становится обычным словарём без каких-либо выдающихся методов, вроде most_common.

А вот set() и list() дают на выходе множество и список ключей соответственно — в принципе, точки зрения данных — одно и то же.

from collections import Counter

data = ['hasta', 'la', 'vista', 'baby', 'la', 'vista']
cntr = Counter(data)

#преобразуем в список кортежей
print cntr.items()
Out: [('hasta', 1), ('baby', 1), ('vista', 2), ('la', 2)]

#преобразуем в словарь
print dict(cntr)
Out: {'hasta': 1, 'baby': 1, 'vista': 2, 'la': 2}

#преобразуем в список
print list(cntr)
Out: ['hasta', 'baby', 'vista', 'la']

#преобразуем во множество
print set(cntr)
Out: set(['hasta', 'baby', 'vista', 'la'])

Операции с каунтерами

А еще между каунтерами можно проводить арифметические и действия — сложение, когда значения ключей складываются и вычитание, которое отличается от метода subtract тем, что в итоговом каунтере остаются только положительные значения. Никаких нулей либо отрицательных чисел, даже не просите!

from collections import Counter

cntr1 = Counter({'hasta': 4, 'vista': 1})
cntr2 = Counter({'hasta': 1, 'vista': 2})

#складываем
print cntr1 + cntr2
Out: Counter({'hasta': 5, 'vista': 3})

#вычитаем
print cntr1 - cntr2
Out: Counter({'hasta': 3})

А еще можно проводить те же операции, что и с множествами — пересечение и объединение. В чём их особенность?

Пересечение двух каунтеров даёт каунтер, где присутствуют только ключи, которые есть в обоих каунтерах с минимальным значением среди этих ключей. Т.е. если в одном каунтере есть связка ‘vista’: 4, а в другом — ‘vista’: 3, то в результате пересечения получим ‘vista’: 3. При этом, само собой, если пересекающихся ключей нет, то в результате получим пустой каунтер.

При объединении мы получаем те связки ключ-значение, которые есть либо в одном, либо в другом каунтере (что очевидно). Если оказываются пересекающиеся ключи, то предпочтение отдается ключу с максимальным значением. То есть, если в одном каунтере есть связка ‘vista’: 4, а в другом — ‘vista’: 3, то в результате пересечения получим ‘vista’: 4.

Само собой, пример нагляднее:

from collections import Counter

cntr1 = Counter({'hasta': 4, 'vista': 1, 'pasta': 7})
cntr2 = Counter({'hasta': 1, 'vista': 2})

#Проводим пересечение
print cntr1 & cntr2
Out: Counter({'hasta': 1, 'vista': 1})

#Проводим объединение
print cntr1 | cntr2
Out: Counter({'pasta': 7, 'hasta': 4, 'vista': 2})

Практическое применение

Каково же практическое применение данного класса для задач обработки естественного языка?

С ходу в голову приходят такие идеи:

  1. Частотный анализ текстов
    1. Анализ наиболее/наименее частотных слов в тексте либо в корпусе текстов конкретного автора
    2. Сравнение наиболее/наименее частотной лексики у разных авторов или в разных областях знаний
  2. Использование при расчёте TF-IDF (Term Frequency — Inverse Document Frequency). TF для каждого слова рассчитывается как количество раз, когда слово встретилось в тексте, деленное на общее количество слов в тексте. Вот как-то так, например:

def compute_tf(text):
        #преобразуем входной список в каунтер
        tf_text = Counter(text)
        #используем генератор словарей для деления значения каждого элемента
        #в каунтере на общее число слов в тексте - т.е. длину списка слов.
        tf_text = {i: tf_text[i]/float(len(text)) for i in tf_text}
        return tf_text

text = ['hasta', 'la', 'vista', 'baby', 'la', 'vista']
print compute_tf(text)
Out: {'hasta': 0.166666666667, 'baby': 0.166666666667, 'vista': 0.333333333333, 'la': 0.333333333333}

Полезные ссылки

Описание Counter в стандартной документации Python 2.7 (англ.)

Описание Counter в переводе стандартной документации для Python 3 (рус.)

Википедия о TF-IDF (рус.)

4,039 просмотров всего, 1 просмотров сегодня

Стандартная библиотека Python — для обработки языка: collections.Counter
5 1 vote

Leave a Reply

2 Comments on "Стандартная библиотека Python — для обработки языка: collections.Counter"


Guest
Александр
9 months 23 days ago

Ошибка в расчете TF-IDF в выражении: tf_text = {i: tf_text[i]/float(len(tf_text)) for i in tf_text}. Необходимо заменить tf_text на text

9 months 10 days ago

Ага, исправил. И как я сразу не заметил.