Counter python: Модуль collections | Python 3 для начинающих и чайников

Содержание

Модуль collections | Python 3 для начинающих и чайников

Модуль collections — предоставляет специализированные типы данных, на основе словарей, кортежей, множеств, списков.

Первым рассматриваемым типом данных будет Counter.

collections.Counter

collections.Counter — вид словаря, который позволяет нам считать количество неизменяемых объектов (в большинстве случаев, строк). Пример:

>>> import collections
>>> c = collections.Counter()
>>> for word in ['spam', 'egg', 'spam', 'counter', 'counter', 'counter']:
...     c[word] += 1
...
>>> print(c)
Counter({'counter': 3, 'spam': 2, 'egg': 1})
>>> print(c['counter'])
3
>>> print(c['collections'])
0

Но возможности Counter на этом не заканчиваются. У него есть несколько специальных методов:

elements() — возвращает список элементов в лексикографическом порядке.

>>> c = Counter(a=4, b=2, c=0, d=-2)
>>> list(c.elements())
['a', 'a', 'a', 'a', 'b', 'b']

most_common([n]) — возвращает n наиболее часто встречающихся элементов, в порядке убывания встречаемости. Если n не указано, возвращаются все элементы.

>>> Counter('abracadabra').most_common(3)
[('a', 5), ('r', 2), ('b', 2)]

subtract([iterable-or-mapping]) — вычитание

>>> c = Counter(a=4, b=2, c=0, d=-2)
>>> d = Counter(a=1, b=2, c=3, d=4)
>>> c.subtract(d)
Counter({'a': 3, 'b': 0, 'c': -3, 'd': -6})

Наиболее часто употребляемые шаблоны для работы с Counter:

  • sum(c.values()) — общее количество.
  • c.clear() — очистить счётчик.
  • list(c) — список уникальных элементов.
  • set(c) — преобразовать в множество.
  • dict(c) — преобразовать в словарь.
  • c.most_common()[:-n:-1] — n наименее часто встречающихся элементов.
  • c += Counter() — удалить элементы, встречающиеся менее одного раза.

Counter также поддерживает сложение, вычитание, пересечение и объединение:

>>> c = Counter(a=3, b=1)
>>> d = Counter(a=1, b=2)
>>> c + d
Counter({'a': 4, 'b': 3})
>>> c - d
Counter({'a': 2})
>>> c & d
Counter({'a': 1, 'b': 1})
>>> c | d
Counter({'a': 3, 'b': 2})

Следующими на очереди у нас очереди (deque)

collections.deque

collections.deque(iterable, [maxlen]) — создаёт очередь из итерируемого объекта с максимальной длиной maxlen. Очереди очень похожи на списки, за исключением того, что добавлять и удалять элементы можно либо справа, либо слева.

Методы, определённые в deque:

append(x) — добавляет x в конец.

appendleft(x) — добавляет x в начало.

clear() — очищает очередь.

count(x) — количество элементов, равных x.

extend(iterable) — добавляет в конец все элементы iterable.

extendleft(iterable) — добавляет в начало все элементы iterable (начиная с последнего элемента iterable).

pop

() — удаляет и возвращает последний элемент очереди.

popleft() — удаляет и возвращает первый элемент очереди.

remove(value) — удаляет первое вхождение value.

reverse() — разворачивает очередь.

rotate(n) — последовательно переносит n элементов из начала в конец (если n отрицательно, то с конца в начало).

collections.defaultdict

collections.defaultdict ничем не отличается от обычного словаря за исключением того, что по умолчанию всегда вызывается функция, возвращающая значение:

>>> import collections
>>> defdict = collections.defaultdict(list)
>>> print(defdict)
defaultdict(<class 'list'>, {})
>>> for i in range(5):
...     defdict[i].append(i)
...
>>> print(defdict)
defaultdict(<class 'list'>, {0: [0], 1: [1], 2: [2], 3: [3], 4: [4]})

collections.OrderedDict

collections.OrderedDict — ещё один похожий на словарь объект, но он помнит порядок, в котором ему были даны ключи. Методы:

popitem(last=True) — удаляет последний элемент если last=True, и первый, если last=False.

move_to_end(key, last=True) — добавляет ключ в конец если last=True, и в начало, если last=False.

>>> d = {'banana': 3, 'apple':4, 'pear': 1, 'orange': 2}
>>> OrderedDict(sorted(d.items(), key=lambda t: t[0]))
OrderedDict([('apple', 4), ('banana', 3), ('orange', 2), ('pear', 1)])
>>> OrderedDict(sorted(d.items(), key=lambda t: t[1]))
OrderedDict([('pear', 1), ('orange', 2), ('banana', 3), ('apple', 4)])
>>> OrderedDict(sorted(d.items(), key=lambda t: len(t[0])))
OrderedDict([('pear', 1), ('apple', 4), ('orange', 2), ('banana', 3)])

collections.namedtuple()

Класс collections.namedtuple позволяет создать тип данных, ведущий себя как кортеж, с тем дополнением, что каждому элементу присваивается имя, по которому можно в дальнейшем получать доступ:

>>> Point = namedtuple('Point', ['x', 'y'])
>>> p = Point(x=1, y=2)
>>> p
Point(x=1, y=2)
>>> p.x
1
>>> p[0]
1

Не изобретать велосипед, или Обзор модуля collections в Python

Типы данных Python не ограничиваются стандартными. Модуль collections содержит специализированные классы контейнеров, альтернативных традиционным dict, list и tuple.

Это доступный «из коробки» родной модуль Python – те самые батарейки, что идут в комплекте. Уверенное владение инструментарием collections, itertools и других модулей стандартной библиотеки – одна из черт, отличающих продвинутых питонистов от новичков.

Рассмотрим на примерах самые популярные составляющие модуля collections для Python 3 (проверено на 3.6). Для начала импортируйте библиотеку:

        import collections
    

Одна из распространённых задач, для которой начинающие питонисты придумывают собственные решения, – подсчёт элементов последовательности: списка, строки символов и т. д.

Если нужно что-то посчитать, определить количество вхождений или наиболее (наименее) часто встречающихся элементов, используйте объекты класса

Counter. Создаются они с помощью конструктора collections.Counter().

Функция принимает итерируемый аргумент и возвращает словарь, в котором ключами служат индивидуальные элементы, а значениями – количества повторений элемента в переданной последовательности. Посчитаем, сколько раз встречается каждая буква в слове «абракадабра»:

        >>> list_of_letters = list('абракадабра')
>>> letter_cnt = collections.Counter(list_of_letters)
>>> letter_cnt
Counter({'а': 5, 'б': 2, 'р': 2, 'к': 1, 'д': 1})
    

Обращение к ключам происходит аналогично обычному словарю:

        >>> letter_cnt['а']
5
    

Если элемент отсутствовал в последовательности, при обращении по ключу счётчик не вызовет исключение, а вернет нулевое значение:

        >>> letter_cnt['ю']
0
    

Присвоение нуля ключу не удаляет это значение, а создаёт соответствующую пару:

        >>> letter_cnt['в'] = 0
>>> letter_cnt
Counter({'а': 5, 'б': 2, 'р': 2, 'к': 1, 'д': 1, 'в': 0})
    

Чтобы удалить пару key-value, используем del:

>>> del letter_cnt['в'] >>> letter_cnt Counter({'а': 5, 'б': 2, 'р': 2, 'к': 1, 'д': 1})

В качестве аргумента конструктор принимает не только последовательность, но и словарь, содержащий результаты подсчёта:

        >>> emotion_cnt = collections.Counter({'like':2, 'dislike':3})
>>> emotion_cnt
Counter({'like': 2, 'dislike': 3})
    

Метод elements() преобразует результаты подсчета в итератор:

        >>> list(emotion_cnt.elements())
['like', 'like', 'dislike', 'dislike', 'dislike']
    

Метод

most_common(n) ищет n самых повторяющихся элементов. Найдём для примера три наиболее частых символа:

        # без передачи аргумента выводятся все элементы
# в порядке от наиболее частых к наиболее редким

>>> letter_cnt.most_common(3)
[('а', 5), ('б', 2), ('р', 2)]
    

Метод возвращает список кортежей вида (ключ, число повторений).

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

        >>> letter_cnt.most_common()[:-3:-1]
[('д', 1), ('к', 1)]
    

Счётчики складываются и вычитаются друг из друга:

        >>> letter_cnt + emotion_cnt
[('д', 1), ('к', 1)]
>>> emotion_cnt - collections.Counter(like=1, dislike=3)
Counter({'like': 1})
    

Операнд & даст минимальные значения для одних и тех же подсчитываемых элементов, операнд | – максимальные:

        >>> c = collections.Counter(a=4, b=2, c=0, d=-2)
>>> d = collections.Counter(a=1, b=2, c=3, d=4)
>>> c & d
Counter({'b': 2, 'a': 1})
>>> c | d
Counter({'a': 4, 'd': 4, 'c': 3, 'b': 2})
    

Как видно из примера, счётчику можно передавать отрицательные значения. Однако для перечисленных операций хранятся только положительные подсчеты. Нулевые или отрицательные значения обычно приходится хранить при вычитании, что реализовано в методе

subtract():

        >>> c.subtract(d)
>>> c
Counter({'a': 3, 'b': 0, 'c': -3, 'd': -6})
    

Обратите внимание, что метод subtract() обновляет сам счётчик, а не создает новый.

Распространненные шаблоны применения Counter:

        >>> sum(letter_cnt.values())  # число всех посчитанных элементов
11
>>> list(letter_cnt)  # список уникальных элементов исходной последовательности
['а', 'б', 'р', 'к', 'д']
>>> set(letter_cnt)
{'а', 'б', 'д', 'к', 'р'}
>>> dict(letter_cnt)  # счетчик это подкласс словаря, можно преобразовать в обычный dict
{'а': 5, 'б': 2, 'р': 2, 'к': 1, 'д': 1}

Унарные операции оставляют только положительные или отрицательные подcчёты:

        >>> +c  # способ вывести положительные подсчеты
Counter({'a': 3})
>>> -c # способ вывести отрицательные подсчеты
Counter({'c': 3, 'd': 6})
>>> c.clear()  # Очищаем счетчик
>>> c
Counter()
    

Счетчик в сочетании с регулярными выражениями используется для частотного анализа текста. Давайте узнаем, какие десять слов чаще прочих встречаются в тексте «Евгения Онегина»:

        >>> import re
>>> words = re.findall(r'\w+', open('onegin.txt').read().lower())
>>> collections.Counter(words).most_common(10)
[('и', 1011),
 ('в', 606),
 ('не', 387),
 ('он', 294),
 ('на', 260),
 ('с', 240),
 ('я', 238),
 ('как', 192),
 ('но', 190),
 ('что', 167)]
    

Что будет, если обратиться к словарю по ключу, которого в нем ещё нет?

Верно, исключение KeyError:

        >>> d = dict()
>>> d['name'] = 'James' 
>>> d['surname'] = 'Bond'
>>> d['patronymic']
KeyError Traceback (most recent call last) <...>
    

Если нет нужды отлавливать исключение, достаточно использовать альтернативный вариант словаря – collections.defaultdict.

Соответствующему конструктору в качестве аргумента передается тип элемента по умолчанию:

        >>> d = collections.defaultdict(str)
>>> d['name'] = 'James' 
>>> d['surname'] = 'Bond'
>>> d['patronymic']
''
>>> d
defaultdict(str, {'name': 'James', 'surname': 'Bond', 'patronymic': ''})
    

Таким образом, для ключей, к которым происходит обращение, конструктор поставит в соответствие дефолтный элемент данного типа. В случае str – пустая строка, для целых чисел – 0 и т. д.

Обычные словари имеют метод setdefault(), который позволяет добиться того же результата, но его использование делает программный код менее наглядным и замедляет исполнение.

Помимо str и int, defaultdict часто используют в связке с пустым списком, чтобы начинать добавление элементов без лишнего кода:

        >>> dict_of_lists = collections.defaultdict(list)
>>> for i in range(5):
...     dict_of_lists[i].append(i)
... 
>>> dict_of_lists
defaultdict(<class 'list'>, {0: [0], 1: [1], 2: [2], 3: [3], 4: [4]})
    

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

Ощутимость пользы OrderedDict так повлияла на обычный dict, что в новых версиях Python различий между ними становится всё меньше. В былые времена OrderedDict кардинально отличался от обычного словаря тем, что умел запоминать порядок вставки. Но с версии Python 3.6 на это способен и обычный словарь. Однако некоторые различия между ними все равно остаются:

  1. Обычный dict был разработан, чтобы быть лучшим в операциях, связанных с мапированием. Отслеживание порядка вставки для него – дело вторичное. И наоборот, OrderedDict хорош в операциях переупорядочения, а эффективность, скорость итераций и производительность не главное.
  2. Алгоритмически OrderedDict может обрабатывать частые операции переупорядочения лучше, чем dict.

Так как OrderedDict это упорядоченная последовательность, объекты содержат соответствующие методы, реорганизующие структуру:

  1. popitem(last=True) – удаляет последний элемент если last=True, и первый, если last=False
  2. move_to_end(key, last=True) – переносит ключ key в конец, если last=True, и в начало, если last=False
        >>> d = collections.OrderedDict.fromkeys('abcde')
>>> d.move_to_end('b')
>>> ''.join(d.keys())
'acdeb'
>>> d.move_to_end('b', last=False)
>>> ''.join(d.keys())
'bacde'
    

После разговора о словарях самое время обсудить класс, умеющий объединять словари в надструктуру – ChainMap. При этом получается не один общий словарь, а их совокупность, в которой каждый словарь остаётся независимой составляющей:

        >>> letters = {'a':1, 'b':2}
>>> vowels = {'a':1, 'b':0, 'c':0, 'd': 0, 'e':1}
>>> chain = collections.ChainMap(letters, vowels)
>>> chain
ChainMap({'a': 1, 'b': 2}, {'a': 1, 'b': 0, 'c': 0, 'd': 0, 'e': 1})
    

При обращении к ChainMap по ключу одного из словарей, происходит поиск значения среди всех словарей, при этом нет необходимости указывать конкретный словарь:

        >>> chain['e']
1
    

При поиске ChainMap выводит первое найденное значение (проходя словари по очереди добавления). В том числе если в словарях несколько одинаковых ключей:

        >>> chain['b']
2
    

Изменение содержания словаря изменяет и ChainMap. Нет необходимости перезаписывать надструктуру:

        >>> letters['c'] = 3
>>> chain
ChainMap({'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'b': 0, 'c': 0, 'd': 0, 'e': 1})
    

Так как ChainMap это комбинация словарей, логично, что у неё есть методы keys() и values():

        >>> list(chain.keys())
['c', 'd', 'a', 'e', 'b']
>>> list(chain.values())
[3, 0, 1, 1, 2]
    

Значения values соответствуют списку keys, как это было описано выше. То есть в случае несколько совпадающих ключей, выводится значение для первого из словарей, где встречается этот ключ.

При необходимости расширить составленный ранее ChainMap можно методом new_child():

        >>> consons = {'a':0, 'b':1, 'c':1}
>>> chain.new_child(consons)
ChainMap({'a': 0, 'b': 1, 'c': 1}, {'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'b': 0, 'c': 0, 'd': 0, 'e': 1})
    

Обратите внимание, что метод не обновляет старую структуру, а создаёт новую.

Объект типа deque (читается как «дэк», двусторонняя или двусвязная очередь) является усовершенствованным вариантом списка с оптимизированной вставкой/удалением элементов с обоих концов. Реализация deque оптимизирована так, что операции слева и справа имеют примерно одинаковую производительность O(1). Добавление новых элементов в конец происходит не сильно медленнее, чем во встроенных списках, но добавление в начало выполняется существенно быстрее.

        >>> seq = list("bcd")
>>> deq = collections.deque(seq)
>>> deq
deque(['b', 'c', 'd'])
>>> deq.append('e')      # добавление в конец
>>> deq.appendleft('a')  # добавление в начало (левый конец)
>>> deq
deque(['a', 'b', 'c', 'd', 'e'])
    

Чтобы добавлять не одиночный элемент, а группу итерируемого объекта iterable используйте соответственно extend(iterable) и extendleft(iterable).

Аналогично методу append() метод pop() для deque работает с обоих концов:

        >>> deq.pop()
>>> deq.popleft()
>>> deq
deque(['b', 'c', 'd'])
    

Если нужно посчитать число вхождений элемента в последовательность, применяем метод count():

        >>> deq.count('b'), deq.count('a')
(1, 0)
    

Кроме перечисленных, доступны следующие методы:

  1. remove(value) – удаление первого вхождения value
  2. reverse() – разворачивает очередь)
  3. rotate(n=1) – последовательно переносит n элементов из начала в конец (если n отрицательно, то с конца в начало). В этом поведение deque напоминает кольцевой связный список

Очередь deque имеет аргумент maxlen, позволяющий ограничить ее размер. При заполнении ограниченной очереди добавление n новых объектов «слева» вызовет удаление n элементов справа.

Ограниченные очереди обеспечивают функциональность, похожую на tail-фильтр в Unix:

        def tail(filename, n=10):
    """Возвращает n последних строк файла'"""
    with open(filename) as f:
        return collections.deque(f, n)
    

Другой шаблон применения deque – хранение последних добавленных элементов с выбрасыванием более старых. Пример компактной и быстрой реализации функции скользящего среднего:

        import itertools

def moving_average(iterable, n=3):
    # moving_average([40, 30, 50, 46, 39, 44]) --> 40.0 42.0 45.0 43.0
    it = iter(iterable)
    d = collections.deque(itertools.islice(it, n-1))
    d.appendleft(0)
    s = sum(d)
    for elem in it:
        s += elem - d.popleft()
        d.append(elem)
        yield s / n
    

Алгоритм распределения нагрузки Round-robin можно реализовать с помощью итераторов, хранящихся в deque. Значения выводятся из активного итератора в нулевой позиции. Если этот итератор исчерпан, его можно удалить методом popleft (); в противном случае его можно циклически «провернуть» до конца методом rotate():

        def roundrobin(*iterables):
    "roundrobin('ABC', 'D', 'EF') --> A D E B F C"
    iterators = collections.deque(map(iter, iterables))
    while iterators:
        try:
            while True:
                yield next(iterators[0])
                iterators.rotate(-1)
        except StopIteration:
            # Удалить "закончившийся" итератор
            iterators.popleft()
    

namedtuple() – функция-фабрика для создания именованных кортежей. Этот тип данных похож на struct в других языках программирования:

        >>> cols = ['fname', 'pname', 'lname', 'age']
>>> User = collections.namedtuple('User', cols)
>>> user1 = User('Петр', 'Иванович', 'Сидоров', 30)
>>> user1
User(fname='Петр', pname='Иванович', lname='Сидоров', age=30)
>>> user1.lname
Сидоров
>>> Point = collections.namedtuple('Point', ['x', 'y'])
>>> p = Point(3, 4)
>>> p.x**2 + p.y**2
25
    

Именованные кортежи делают код яснее – вместо индексирования составляющие объекта вызываются по явным именам. Остаётся доступной и численная индексация:

        >>> p[0]**2 + p[1]**2
25
    

Именованные кортежи часто используются для назначения имён полей кортежам, возвращаемым модулями csv или sqlite3:

        EmployeeRecord = collections.namedtuple('EmployeeRecord', 'name, age, title, department, paygrade')

import csv
for emp in map(EmployeeRecord._make, csv.reader(open("employees.csv", "rb"))):
    print(emp.name, emp.title)

import sqlite3
conn = sqlite3.connect('/companydata')
cursor = conn.cursor()
cursor.execute('SELECT name, age, title, department, paygrade FROM employees')
for emp in map(EmployeeRecord._make, cursor.fetchall()):
    print(emp.name, emp.title)
    

Структура namedtuple похожа на словарь. Посредством метода _asdict можно представить те же данные в виде OrderedDict:

        >>> p._asdict()
OrderedDict([('x', 3), ('y', 4)])
    

Чтобы вызвать значение через строковый ключ, необязательно преобразовывать namedtuple – подходит стандартная функция getattr():

        >>> getattr(p, 'x')
3
    

Чтобы преобразовать словарь в именованный кортеж заданного типа, достаточно распаковать его оператором **:

        >>> d = {'x': 0, 'y': 1}
>>> Point(**d)
Point(x=0, y=1)
    

Имена полей namedtuple перечислены в _fields:

        >>> user1._fields, p._fields
(('fname', 'pname', 'lname', 'age'), ('x', 'y'))
    

С версии 3.7 можно присвоить полям значения по умолчанию.

Поскольку именованный кортеж является обычным классом Python, в него легко привнести новую функциональность или изменить старую. Например, добавим к Point расчёт гипотенузы и формат вывода данных:

        class Point(collections.namedtuple('Point', ['x', 'y'])):
    __slots__ = ()  # предотвращает создание словарей экземпляров
    @property
    def hypot(self):
        return (self.x**2 + self.y**2) ** 0.5
    def __str__(self):
        return 'Point: x=%6.3f  y=%6.3f  hypot=%6.3f' % (self.x, self.y, self.hypot)

for p in Point(3, 4), Point(14, 5/7):
    print(p)
    
        Point: x= 3.000  y= 4.000  hypot= 5.000
Point: x=14.000  y= 0.714  hypot=14.018
    

Если вам пришлась по душе компактность namedtuple в сравнении с обычными классами и ваш проект может работать с версиями Python не меньше 3.7, присмотритесь к модулю dataclasses. Эта встроенная библиотека предоставляет декоратор и функции для автоматического добавления в пользовательские классы сгенерированных специальных методов, таких как __init__() и __repr__().

Подведём итог нашему рассказу об основных составляющих модуля collections:

  1. Counter – инструмент подсчёта неизменяемых объектов. Используйте, если нужно определить количество вхождений или число наиболее (наименее) часто встречающихся элементов.
  2. defaultdict – словарь, умеющий при вызове отсутствующего ключа вместо вызова исключения KeyError записывать значение по умолчанию (работает быстрее, чем метод setdefault()).
  3. OrderedDict – словарь с памятью порядка добавления элементов, умеющий переупорядочивать элементы лучше, чем dict.
  4. ChainMap – контейнер комбинаций словарей с поиском, обобщением ключей и элементов.
  5. namedtuple() – функция-фабрика для создания именованного кортежа. Это один из простейших способов сделать код более ясным: использовать вместо индексов имена.
  6. deque – двусторонняя очередь – список, оптимизированный для вставки и удаления элементов с обоих концов с методом подсчёта вхождений
  7. UserDict, UserList, UserString – не заслуживающие развёрнутого описания обертки над стандартными объектами словарей, списков и строк для беспроблемного наследования (прямое наследование встроенным типам dict, list, str чревато ошибками, связанными с игнорированием переопределения методов).

Также у модуля collections имеется наследованный модуль коллекции абстрактных базовых классов сollections.abc. Но это тема отдельного разговора.

Операции с Python Counter | Портал информатики для гиков

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

  

from collections import Counter

s1 ='aabbbcccdeff'

c1 = Counter(s1)

print("c1 :", c1)

  

L1 =[1, 2, 1, 1, 4, 4, 4, 5, 6, 6, 3, 3, 0, 0]

t1 = Counter(L1)

  

print("t1 :", t1)

Выход:

c1 : Counter({'b': 3, 'c': 3, 'a': 2, 'f': 2, 'e': 1, 'd': 1})
t1 : Counter({1: 3, 4: 3, 0: 2, 3: 2, 6: 2, 2: 1, 5: 1})

Выполнение определенных операций с возвращенным выходным словарем, если d является выходным словарем, например d.items (), d.keys (), d.values ()

from collections import Counter

d ='aabbbcccdeff'

d = Counter(d)

  

print("d :", d) 

  

print("d.values() : ", d.values())  

  

print("d.items() :", d.items())

  

print("d.keys() :", d.keys())

  

print("sorted(d) :", sorted(d))

Выход:

d : Counter({'b': 3, 'c': 3, 'a': 2, 'f': 2, 'e': 1, 'd': 1})
d.values() :  dict_values([2, 3, 3, 2, 1, 1])
d.items() : dict_items([('a', 2), ('b', 3), ('c', 3), ('f', 2), ('e', 1), ('d', 1)])
d.keys() : dict_keys(['a', 'b', 'c', 'f', 'e', 'd'])
sorted(d) : ['a', 'b', 'c', 'd', 'e', 'f']

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

from collections import Counter

t1 = Counter('aabbddffggjik')

t2 = Counter('aaabbbssshhhggdkkll')

  

print("t1:", t1)

print("t2:", t2)

print("t1 + t2 :", t1 + t2)

Выход:

t1: Counter({'g': 2, 'a': 2, 'b': 2, 'f': 2, 'd': 2, 'k': 1, 'j': 1, 'i': 1})
t2: Counter({'a': 3, 'b': 3, 'h': 3, 's': 3, 'l': 2, 'g': 2, 'k': 2, 'd': 1})
t1+t2 : Counter({'a': 5, 'b': 5, 'g': 4, 'k': 3, 'h': 3, 'd': 3, 's': 3, 'l': 2, 'f': 2, 'j': 1, 'i': 1})

Вычитание двух счетчиков
Количество общих элементов вычитается друг из друга и (сохраняет только положительные значения)

from collections import Counter

t1 = Counter('aabbddffggjik')

t2 = Counter('aaabbbssshhhggdkkll')

  

print("t1:", t1)

print("t2:", t2)

print("t1-t2 :", t1-t2)

print("t2-t1 :", t2-t1)

Выход:

t1: Counter({'f': 2, 'd': 2, 'b': 2, 'a': 2, 'g': 2, 'k': 1, 'i': 1, 'j': 1})
t2: Counter({'h': 3, 'b': 3, 'a': 3, 's': 3, 'l': 2, 'k': 2, 'g': 2, 'd': 1})
t1-t2 : Counter({'f': 2, 'i': 1, 'j': 1, 'd': 1})
t2-t1 : Counter({'h': 3, 's': 3, 'l': 2, 'k': 1, 'b': 1, 'a': 1})

Пересечения (&) двух счетчиков
Intersaction (&) сохранит только минимум соответствующих отсчетов: min (t1 [x], t2 [x]):

from collections import Counter

t1 = Counter('aaabbbbccdeeee')

t2 = Counter('aabbccccdddee')

  

print("t1 :", t1)

print("t2 :", t2)

print("t1&t2 :", t1&t2)

Выход:

t1 : Counter({'e': 4, 'b': 4, 'a': 3, 'c': 2, 'd': 1})
t2 : Counter({'c': 4, 'd': 3, 'a': 2, 'e': 2, 'b': 2})
t1&t2 : Counter({'c': 2, 'a': 2, 'e': 2, 'b': 2, 'd': 1})

Союз (|) двух счетчиков
Union (|) сохранит только максимум соответствующих отсчетов: max (c [x], d [x]):

from collections import Counter

t1 = Counter('aaabbbbccdeeee')

t2 = Counter('aabbccccdddee')

  

print("t1 :", t1)

print("t2 :", t2)

print("t1|t2 :", t1|t2)

Выход:

t1 : Counter({'b': 4, 'e': 4, 'a': 3, 'c': 2, 'd': 1})
t2 : Counter({'c': 4, 'd': 3, 'a': 2, 'b': 2, 'e': 2})
t1|t2 : Counter({'b': 4, 'e': 4, 'c': 4, 'a': 3, 'd': 3})

Рекомендуемые посты:

Операции с Python Counter

0.00 (0%) 0 votes

Стандартная библиотека Python — для обработки языка: collections.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 (рус.)