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

Одна из самых частых мелких подзадач, которые мне приходится делать при анализе данных, — для категориального признака определить число вхождений для каждой категории. Есть много способов её решения — я постарался описать всё, что пришли в голову на языке 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.

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

 

Считаем категории: 9 комментариев

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

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

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

  2. Добрый день. Возможно немного не по теме, но все же напишу тут.
    Пытался делать данный метод в с помощью метода Svm
    import numpy as np
    X = np.array([[-1, -1, 2], [-2, -1, 4], [1, 1, 5], [2, 1, 2]]) #Тут массив из 3
    y = np.array([2, 2, 3, 2])
    from sklearn.svm import SVC
    clf = SVC(probability=True) #Обязательно задать для расчета вероятностей
    clf.fit(X, y)

    SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
    decision_function_shape=’ovr’, degree=3, gamma=’auto’, kernel=’rbf’,
    max_iter=-1, probability=True, random_state=None, shrinking=True,
    tol=0.001, verbose=False)
    print(clf.predict([[-1, -1, 2]])) # И тут массив из 3
    print(clf.classes_)
    print(clf.predict_proba([[-1, -1, 2]]))

    Удивительная вещь.
    Получаем Output:
    [2] Ответ predict
    [2 3] #Сортировка классов
    [[ 0.37438294 0.62561706]] #Вероятности классов
    Долгий поиск в гугле выдал две версии данного, а также формулу P(y|X) = 1 / (1 + exp(A * f(X) + B))
    Где where f(X) is the signed distance of a sample from the hyperplane (scikit-learn’s decision_function method)
    Но по факту метод реально всегда выдает Predict по классу с наименьшим числом, я пробовал для 5 классов. Почему? Ведь судя по формуле,чем меньше расстояние F(x),тем выше должна быть вероятность.
    Может вы прольете свет на данное недоразумение?

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

  4. А еще там же есть clf.decision_function.
    Вон он таки как раз выбирает по максимальному числу, но тоже непонятно по какому принципу, так как там присутствуют отрицательные значения.
    Хочется один раз в этом разобраться, чтобы дальше смело использовать в серьезных проектах ничего не боясь. =)

    • Да, совсем не по теме… но проблема интересная. Чтобы «заработало правильно» надо увеличить выборку: сделайте

      X = np.array([[-1, -1, 2], [-2, -1, 4], [1, 1, 5], [2, 1, 2]] * 10) #Тут массив из 3

      y = np.array([2, 2, 3, 2] * 10)

      Теперь всё будет правильно. Связано это с тем, что predict_proba использует калибровку
      https://www.csie.ntu.edu.tw/~cjlin/papers/plattprob.pdf
      (см. Platt’s approach) ну и там внутри используется подбор нужных параметров с помощью 5-fold-CV, в итоге на малых выборках будет баг!

      clf.decision_function — выдаёт расстояние до разделяющей гиперплоскости. Оно меньше нуля, если объект лежит по другую сторону (т.е. явно не принадлежит этому классу).

      Можно даже целый пост сделать об этом.

      • Спасибо Вам огромное, нигде не мог найти ответа на этот вопрос. Для меня было очень важно разобраться. Теперь наступила полная ясность!

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

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

Логотип WordPress.com

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

Google+ photo

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

Фотография Twitter

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

Фотография Facebook

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

Connecting to %s