Dataframe: извлечение нескольких родителей из одного идентификатора и количества экземпляров

62
6

Не знаю, хорош ли заголовок. Не стесняйтесь его корректировать!

Здесь ситуация: у меня есть dataframe, который является в основном каталогом продуктов. В этом есть два важных столбца. Один из них - это идентификатор продукта, а один - категория из 12 цифр. Это некоторые примеры данных. Конечно, исходные данные содержат гораздо больше продуктов, больше столбцов и много разных категорий.

products = [
{'category': 110401010601, 'product': 1000023},
{'category': 110401020601, 'product': 1000024},
{'category': 110401030601, 'product': 1000025},
{'category': 110401040601, 'product': 1000026},
{'category': 110401050601, 'product': 1000027}]

pd.DataFrame.from_records(products)

Задача состоит в том, чтобы использовать 12-значный номер категории для формирования родительских категорий и использовать этих родителей для подсчета количества продуктов, соответствующих этой родительской категории. Родительские категории формируются с шагом в 2 цифры. Впоследствии подсчеты для каждого родителя используются для поиска родителя для каждого продукта с минимальным количеством записей (скажем, 12 детей). Конечно, чем короче будет число, тем больше продуктов будет соответствовать этому числу. Вот пример родительской структуры:

110401050601 # product category
1104010506 # 1st parent
11040105 # 2nd parent
110401 # 3rd parent
1104 # 4th parent
11 # 5th super-parent

Вы видите, что может быть много других продуктов, например, 1104, а не только 110401050601.

Идея 1 для небольших данных. До тех пор, пока вы полностью загрузите небольшие или средние данные в кадр данных Pandas, это непростая задача. Я решил это с помощью этого кода. Недостатком является то, что этот код предполагает, что все данные хранятся в памяти, и каждый цикл представляет собой другой выбор в полный фрейм данных, что не очень хорошо с точки зрения производительности. Пример: для 100 000 строк и 6 родительских групп (образованных из 12- DataFrame.loc[...] цифр) вы можете получить 600 000 DataFrame.loc[...] через DataFrame.loc[...] постепенно увеличиваясь (в худшем случае). Чтобы предотвратить это, я нарушаю цикл, если родитель был замечен раньше. Замечание: метод df.shape[0] похож на len(df).

df = df.drop_duplicates()
categories = df['category'].unique()

counts = dict()
for cat in categories:
counts[cat] = df.loc[df['category'] == cat].shape[0]

for i in range(10,1,-2):
parent = cat[:i]

if parent not in counts:
counts[parent] = df.loc[df['category'].str.startswith(parent)].shape[0]
else:
break

counts = {key: value for key, value in counts.items() if value >= MIN_COUNT}

Что приводит к чему-то вроде этого (используя части моих исходных данных):

{'11': 100,
'1103': 7,
'110302': 7,
'11030202': 7,
'1103020203': 7,
'110302020301': 7,
'1104': 44,
'110401': 15,
'11040101': 15,
'1104010106': 15,
'110401010601': 15}

Идея 2 для больших данных с использованием flatmap-reduce: теперь представьте, что у вас гораздо больше данных, которые загружаются по-разному, и вы хотите достичь того же, что и выше. Я думал о flatmap чтобы использовать flatmap чтобы разделить номер категории на своих родителей (один на многие), используя 1-счетчик для каждого родителя, а затем применить groupby-key для получения счета для всех возможных родителей. Преимущество этой версии заключается в том, что ей не нужны все данные сразу и что она не делает никаких выборок в dataframe. Но на плоской схеме количество строк увеличивается в 6 раз (из-за 12-разрядного номера категории, разбитого на 6 групп). Поскольку у Pandas нет метода flatten/flatmap, мне пришлось применить обход с помощью unstack (пояснения см. В этом сообщении).

df = df.drop_duplicates()
counts_stacked = df['category'].apply(lambda cat: [(cat[:i], 1) for i in range(10,1,-2)])
counts = counts_stacked.apply(pd.Series).unstack().reset_index(drop=True)

df_counts = pd.DataFrame.from_records(list(counts), columns=['category', 'count'])
counts = df_counts.groupby('category').count().to_dict()['count']
counts = {key: value for key, value in counts.items() if value >= MIN_COUNT}

Вопрос: Оба решения в порядке, но мне интересно, есть ли более элегантный способ добиться того же результата. Я чувствую, что что-то пропустил.

спросил(а) 2021-01-19T17:07:45+03:00 2 месяца, 3 недели назад
1
Решение
62

Вы можете использовать cumsum здесь

df.category.astype(str).str.split('(..)').apply(pd.Series).replace('',np.nan).dropna(1).cumsum(1).stack().value_counts()
Out[287]:
11 5
1104 5
110401 5
11040102 1
110401050601 1
1104010206 1
110401040601 1
11040101 1
1104010106 1
110401010601 1
110401020601 1
11040104 1
110401030601 1
11040103 1
1104010406 1
1104010306 1
11040105 1
1104010506 1
dtype: int64

ответил(а) 2021-01-19T17:07:45+03:00 2 месяца, 3 недели назад
44

Здесь другое решение, использующее Apache Beam SDK для Python. Это совместимо с большими данными, используя парадигму уменьшения карты. Файл образца должен содержать идентификатор продукта в качестве первого столбца и 12-значную категорию как второй столбец с использованием ; как разделитель. Элегантность этого кода заключается в том, что вы можете прекрасно видеть каждое преобразование на строку.

# Python 2.7

import apache_beam as beam
FILE_IN = 'my_sample.csv'
SEPARATOR = ';'

# the collector target must be created outside the Do-Function to be globally available
results = dict()

# a custom Do-Function that collects the results
class Collector(beam.DoFn):
def process(self, element):
category, count = element
results[category] = count
return { category: count }

# This runs the pipeline locally.
with beam.Pipeline() as p:
counts = (p
| 'read file row-wise' >> beam.io.ReadFromText(FILE_IN, skip_header_lines=True)
| 'split row' >> beam.Map(lambda line: line.split(SEPARATOR))
| 'remove useless columns' >> beam.Map(lambda words: words[0:2])
| 'remove quotes' >> beam.Map(lambda words: [word.strip('\"') for word in words])
| 'convert from unicode' >> beam.Map(lambda words: [str(word) for word in words])
| 'convert to tuple' >> beam.Map(lambda words: tuple(words))
| 'remove duplicates' >> beam.RemoveDuplicates()
| 'extract category' >> beam.Map(lambda (product, category): category)
| 'create parent categories' >> beam.FlatMap(lambda cat: [cat[:i] for i in range(12,1,-2)])
| 'group and count by category' >> beam.combiners.Count.PerElement()
| 'filter by minimum count' >> beam.Filter(lambda count: count[1] >= MIN_COUNT)
| 'collect results' >> beam.ParDo(collector)
)

result = p.run()
result.wait_until_finish()

# investigate the result;
# expected is a list of tuples each consisting of the category and its count
print(results)

Код написан на Python 2.7, так как Apache Beam SDK для Python еще не поддерживает Python 3.

ответил(а) 2021-01-19T17:07:45+03:00 2 месяца, 3 недели назад
Ваш ответ
Введите минимум 50 символов
Чтобы , пожалуйста,
Выберите тему жалобы:

Другая проблема