Считаем категории

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

count

Представим, что у нас есть какой-то категориальный признак, например, как на рисунке

data_frame

Мы должны получить ответ в виде
{‘London’: 3, ‘Moscow’: 2, ‘Paris’: 1},
пусть это будет словарь.

1. Pandas

В библиотеке Pandas есть функция value_counts, которая сделает всё нужное. Её недостаток в том, что она работает только со столбцами дата-фрейма, т.е.  описанной ниже функции в качестве аргумента список уже не передашь…

def pandas_vc(lst):
    return (lst.value_counts().to_dict())
pandas_vc(df.feature)

В Pandas можно и группировку приспособить под решение этой задачи

df.groupby(‘feature’)[‘feature’].count().to_dict()

Но этот код нельзя элегантно оформить в виде независимой функции.

2. Python

Попробуем обойтись средствами стандартного Питона. Теперь даже со списками можно работать.

def clear_python_vc(lst):
    result = {}
    for key in lst:
        if key not in result:
           result[key] = 0
        result[key] += 1
    return (result)
clear_python_vc(df.feature)

Если использовать словарь из коллекций, то код немного упрощается (не надо проверять наличие ключа в словаре):

from collections import defaultdict

def collections_vc(lst):
    result = defaultdict(int)
    for key in lst:
       result[key] += 1
    return dict(result)
collections_vc(df.feature)

3. Numpy

Теперь попробуем задействовать библиотеку Numpy. Сначала заведомо плохой вариант (np.sum очень режет глаз):

def numpy_vc(lst):
    return {val: np.sum(lst == val) for val in np.unique(lst)}
numpy_vc(df.feature)

Кстати, этот код ещё и неправильный! Попробуйте догадаться, почему;)

Теперь более эффектный вариант! Обратите внимание, что функция unique возвращает не только уникальные элементы, но и сколько раз они встретились (не все об этом знают):

def unique_vc(lst):
    uniques, count = np.unique(lst, return_counts=True)
    return (dict({u: c for u, c in zip(uniques, count)}))
unique_vc(df.feature)

4. Counter

Если уж мы начали использовать коллекции, то полезно вспомнить, что там есть даже специальный класс, который решает нашу задачу. Проблема в том, что на выходе получаем не совсем словарь:

from collections import Counter
Counter(df.feature) # Counter({'blue': 3, 'red': 2, 'yellow': 1})

5. Методы с ограничениями

Ещё есть несколько способов, которые не годятся для произвольных категориальных признаков, а лишь для списков(!), которые содержат значения 0, 1, 2, …, k.

def count_vc(lst):
    return({x: lst.count(x) for x in set(lst)})
count_vc(lst)

Есть, кстати, функция bincount. «Выходцы из Матлаба» узнают в ней старую добрую accumarray. С её помощью тоже можно решить нашу задачу:

def bincount_vc(lst):
    return({t: x for t, x in enumerate(np.bincount(lst))})
bincount_vc(lst)

Теперь делайте ставки, какой способ самый быстрый, а ниже я раскрою секрет…

Эксперименты

Запустим все функции на выборках разной длины (от 10 до 1 млн). Число категорий здесь равно корню из длины выборки.

pic1.png

Можно по-другому выбирать число категорий, принципиально картина не изменится. Вот что будет, если число категорий равно логарифму от длины выборки:

pic3.png

Все масштабы логарифмические, чтобы графики были красивее. Функция unique показывает образцовую стабильность, а вот value_counts хороша лишь для больших выборок (> 100 000).

Результаты, конечно, могут зависеть от числа категорий, поэтому проварьируем их число для выборки из 100 000 элементов.

pic2.png

П.С. Не забыл ли я какой-нибудь ещё способ?

Есть, кстати, подобный обзор для более общей задачи. Там groupby показывает поведение похожее на value_counts.

Весь код доступен на гитхабе.

 

Реклама

Считаем категории: 4 комментария

  1. Пока не догадался, почему код с np.sum() неправильный, но зато нашел опечатку: «Кстати, этот код не ещё и неправильный!» -> «Кстати, этот код ещё и неправильный!». Глазом, правда, не порезался, вполне логичным кажется просуммировать случаи совпадения значений списка с целевым, но, может я еще не достиг дзена Python, потому мне такое и кажется нормальным 🙂 Отдельное спасибо за информацию о словаре из коллекций, все время до этого приходилось писать не очень красивую конструкцию с проверкой существования ключа в словаре (вот уж точно режет глаз). В целом, статья чудесная, довольно подробное и последовательное изложение, очень не хватало мне такого во время обучения в вузе.

    • Спасибо! Неправильный, т.к. на вход нельзя, подать, например, список. Выражение lst == val применимо только к тому, «что можно сравнивать»…

      А np.sum режет глаз опять же, поскольку суммирование вызывается нампаевское. Если уж вызывать функцию, передавая ей столбец дата-фрейма, то лучше использовать встроенный метод (lst == val).sum()

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s