Назначьте число для каждого уникального значения в списке

У меня есть список строк. Я хочу назначить уникальный номер каждой строке (точное число не важно) и создать список с той же длиной, используя эти цифры, по порядку. Ниже приведена моя лучшая попытка, но я не счастлив по двум причинам:

  • Предполагается, что одни и те же значения находятся рядом друг с другом

  • Мне пришлось запустить список с 0, иначе результат был бы неправильным

Мой код:

names = ['ll', 'll', 'll', 'hl', 'hl', 'hl', 'LL', 'LL', 'LL', 'HL', 'HL', 'HL']
numbers = [0]
num = 0
for item in range(len(names)):
    if item == len(names) - 1:
      break
    elif names[item] == names[item+1]:
        numbers.append(num)
    else:
        num = num + 1
        numbers.append(num)
print(numbers)

Я хочу сделать код более общим, поэтому он будет работать с неизвестным списком. Любые идеи?

Ответ 1

Без использования внешней библиотеки (проверьте EDIT для решения Pandas), вы можете сделать это следующим образом:

d = {ni: indi for indi, ni in enumerate(set(names))}
numbers = [d[ni] for ni in names]

Краткое объяснение:

В первой строке вы присваиваете номер каждому уникальному элементу в своем списке (хранящемся в словаре d, вы можете легко создать его, используя понимание словаря; set возвращает уникальные элементы names);,

Затем во второй строке вы понимаете список и сохраняете фактические числа в списке numbers.

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

# 'll' appears all over the place
names = ['ll', 'll', 'hl', 'hl', 'hl', 'LL', 'LL', 'll', 'LL', 'HL', 'HL', 'HL', 'll']

Это результат для numbers:

[1, 1, 3, 3, 3, 2, 2, 1, 2, 0, 0, 0, 1]

Как вы можете видеть, число 1, связанное с ll, отображается в правильных местах.

ИЗМЕНИТЬ

Если у вас Pandas, вы также можете использовать pandas.factorize:

import pandas as pd

pd.factorize(names)

затем вернет

(array([(array([0, 0, 1, 1, 1, 2, 2, 0, 2, 3, 3, 3, 0]),
 array(['ll', 'hl', 'LL', 'HL'], dtype=object))

Таким образом,

numbers = pd.factorize(names)[0]

Ответ 2

Если условие состоит в том, что числа уникальны и точное число не имеет значения, вы можете построить сопоставление каждого элемента в списке с уникальным числом "на лету", назначив значения из объекта count:

from itertools import count

names = ['ll', 'll', 'hl', 'hl', 'LL', 'LL', 'LL', 'HL', 'll']

d = {}
c = count()
numbers = [d.setdefault(i, next(c)) for i in names]
print(numbers)
# [0, 0, 2, 2, 4, 4, 4, 7, 0]

Вы можете покончить с дополнительными именами, используя map в списке и объект count и задав функцию карты как {}.setdefault (см. комментарий @StefanPochmann):

from itertools import count

names = ['ll', 'll', 'hl', 'hl', 'LL', 'LL', 'LL', 'HL', 'll']
numbers  = map({}.setdefault, names, count()) # call list() on map for Py3
print(numbers)
# [0, 0, 2, 2, 4, 4, 4, 7, 0]

В качестве дополнительного вы также можете использовать np.unique, если у вас уже установлен numpy:

import numpy as np

_, numbers = np.unique(names, return_inverse=True)
print(numbers)
# [3 3 2 2 1 1 1 0 3]

Ответ 3

Чтобы сделать его более универсальным, вы можете обернуть его в функцию, поэтому эти жестко закодированные значения не нанесут никакого вреда, потому что они являются локальными.

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

def your_function(list_of_strings):

    encountered_strings = {}
    result = []

    idx = 0
    for astring in list_of_strings:
        if astring in encountered_strings:  # check if you already seen this string
            result.append(encountered_strings[astring])
        else:
            encountered_strings[astring] = idx
            result.append(idx)
            idx += 1
    return result

И это присвоит индексы в порядке (даже если это не важно):

>>> your_function(['ll', 'll', 'll', 'hl', 'hl', 'hl', 'LL', 'LL', 'LL', 'HL', 'HL', 'HL'])
[0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3]

Для этого требуется только одна итерация по списку строк, что позволяет даже обрабатывать генераторы и т.д.

Ответ 4

Если у вас есть k разные значения, это сопоставляет их целым числам 0 - k-1 в порядке первого появления:

>>> names = ['b', 'c', 'd', 'c', 'b', 'a', 'b']
>>> tmp = {}
>>> [tmp.setdefault(name, len(tmp)) for name in names]
[0, 1, 2, 1, 0, 3, 0]

Ответ 5

Мне удалось немного изменить ваш script, и он выглядит нормально:

names = ['ll', 'hl', 'll', 'hl', 'LL', 'll', 'LL', 'HL', 'hl', 'HL', 'LL', 'HL', 'zzz']
names.sort()
print(names)
numbers = []
num = 0
for item in range(len(names)):
    if item == len(names) - 1:
      break
    elif names[item] == names[item+1]:
        numbers.append(num)
    else:
        numbers.append(num)
        num = num + 1
numbers.append(num)
print(numbers)

Вы можете видеть, что это очень просто, только дело в том, что вместо добавления номера для элемента NEXT я добавляю номер для элемента CURRENT. Все это. О, и сортировка. Сначала он сортирует капитал, а затем строчный регистр в этом примере, вы можете играть с sort(key= lambda:x ...), если хотите изменить это. (Возможно, вот так: names.sort(key = lambda x: (x.upper() if x.lower() == x else x.lower())) )

Ответ 6

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

d = dict()

counter = 0

for name in names:
    if name in d:
        continue
    d[name] = counter
    counter += 1

numbers = [d[name] for name in names]

Ответ 7

Вот аналогичное факторизующее решение с collections.defaultdict и itertools.count:

import itertools as it
import collections as ct


names = ['ll', 'll', 'hl', 'hl', 'LL', 'LL', 'LL', 'HL', 'll']

dd = ct.defaultdict(it.count().__next__)
[dd[i] for i in names]
# [0, 0, 1, 1, 2, 2, 2, 3, 0]

Каждое новое появление вызывает следующее целое число в itertools.count и добавляет новую запись в dd.

Ответ 8

Вы можете попробовать это также: -

names = ['ll', 'll', 'll', 'hl', 'hl', 'hl', 'LL', 'LL', 'LL', 'HL', 'HL', 'HL']

indexList = list(set(names))

print map(lambda name:indexList.index(name),names)