Допустим, у меня есть DataFrame pandas, представляющий оценку крутости для каждого «участника» гипотетического конкурса по дате:
import numpy as np
import pandas as pd
rng = np.random.default_rng()
dates = pd.date_range('2024-08-01', '2024-08-07')
contestants = ['Alligator', 'Beryl', 'Chupacabra', 'Dandelion', 'Eggplant', 'Feldspar']
coolness_score = pd.DataFrame(rng.random((len(dates), len(contestants))), index=dates, columns=contestants)
Alligator Beryl Chupacabra Dandelion Eggplant Feldspar
2024-08-01 0.213901 0.952705 0.801651 0.511080 0.662109 0.486296
2024-08-02 0.495700 0.660502 0.379900 0.778438 0.038616 0.214174
2024-08-03 0.639337 0.036226 0.811501 0.281915 0.101850 0.437146
2024-08-04 0.238590 0.686965 0.357087 0.810922 0.907803 0.370247
2024-08-05 0.712564 0.800191 0.040616 0.503644 0.354333 0.742269
2024-08-06 0.916343 0.299557 0.405399 0.851161 0.336570 0.246618
2024-08-07 0.047052 0.645420 0.823397 0.198483 0.368888 0.168188
Кроме того, каждый участник отнесен к определенной категории, и на каждую категорию накладываются ограничения:
category_mapping = {
'Alligator': 'Animal',
'Beryl': 'Mineral',
'Chupacabra': 'Animal',
'Dandelion': 'Vegetable',
'Eggplant': 'Vegetable',
'Feldspar': 'Mineral'
}
category_limits = {
'Animal': 1,
'Vegetable': 2,
'Mineral': 1
}
Как мне выбрать, какие участники наберут высшие баллы в каждой категории за каждую дату? В частности, учитывая три сценария:
Лучший одиночный результат в каждой категории
Лучшие N баллов в каждой категории, где N одинаково для всех категорий.
Лучшие результаты в каждой категории с ограничениями, определяемыми category_limits
Или, еще лучше, как мне обнулить баллы проигравших?
Сценарии 1 и 2 явно являются подмножествами сценария 3, но я подумал, что здесь могут быть некоторые встроенные функции, которые могут повысить эффективность. Если бы я был предоставлен самому себе, я бы, вероятно, выполнил итерацию по дате, но похоже, что это будет самый медленный из возможных подходов. Спасибо за вашу помощь.
Редактировать 1: В сценарии 3 я имею в виду применять ограничения для каждой категории к каждой дате. Итак, используя приведенный выше пример, это будет первое животное, два лучших овоща и лучший минерал для каждой даты.
Редактировать 2: Удивительные ответы. Должен добавить, что мне также нужна скорость, и мое фактическое приложение имеет размер порядка 250 строк x 15 000 столбцов, около 175 различных категорий, и будет запускаться много-много раз в рамках моделирования Монте-Карло. Я буду тестировать каждое решение, когда у меня будет более ясный ум, но я приветствую любое обсуждение производительности в намеченном масштабе. Спасибо!
import numpy as np
import pandas as pd
rng = np.random.default_rng()
dates = pd.date_range('2024-08-01', '2024-08-07')
contestants = ['Alligator', 'Beryl', 'Chupacabra', 'Dandelion', 'Eggplant', 'Feldspar']
coolness_score = pd.DataFrame(rng.random((len(dates), len(contestants))), index=dates, columns=contestants)
## solution:
# melt dataframe(from wide to long data format)
df = coolness_score.reset_index().rename(columns = {"index": "date"}).melt(id_vars = "date", var_name = "contestant", value_name = "coolness_score")
# map contestant to category and insert category column after date column
category_mapping = {
'Alligator': 'Animal',
'Beryl': 'Mineral',
'Chupacabra': 'Animal',
'Dandelion': 'Vegetable',
'Eggplant': 'Vegetable',
'Feldspar': 'Mineral'
}
df.insert(1, "category", df["contestant"].map(category_mapping))
# sort by date and category for better readability
df = df.sort_values(by=["date", "category"], ignore_index=True)
# the id of the best single score from each category for each day
day_category_top = df.groupby(["date", "category"])[["coolness_score"]].idxmax().rename(columns = {"coolness_score": "best_score_index"})
# 1. The best single score from each category
df.loc[day_category_top["best_score_index"]]
# 2. The best N scores from each category, where N is consistent across all categories
N = 3
df.groupby(["date", "category"]).apply(lambda g: list(g["coolness_score"].nlargest(N)))
это решение предполагает, что в определенный день каждый участник появляется один раз (т. е. один балл в день на каждого участника), о чем и предполагают ваши данные.
для третьего сценария было неясно, имеете ли вы в виду применить ограничение к общему баллу категории или нет. если так:
# 3. The best scores from each category with limits defined by category_limits
category_limits = {
'Animal': 1,
'Vegetable': 2,
'Mineral': 1
}
# sum of the scores for each category, but capped at limit
sum_score = df.groupby(["date", "category"])[["coolness_score"]].sum().reset_index()
sum_score["actual_score"] = sum_score.apply(lambda x: min(x["coolness_score"], category_limits[x["category"]]), axis=1)
sum_score
и вы можете применить аналогичный логин к 1 и 2 к actual_score
.
Вот возможное решение данной проблемы. Сценарий 1: Лучший одиночный результат в каждой категории В первом сценарии мы хотим получить лучший отдельный балл в каждой категории за каждую дату.
def get_top_score_per_category(row):
grouped = row.groupby(category_mapping, axis=0)
return grouped.apply(lambda x: x.idxmax())
top_score_indices = coolness_score.apply(get_top_score_per_category, axis=1)
top_scores = coolness_score.lookup(coolness_score.index, top_score_indices)
Это позволит определить лучшего участника в каждой категории на каждый день.
Сценарий 2: N лучших результатов в каждой категории Если нам нужны N лучших результатов в каждой категории:
N = 2 # Example for the top 2 scores
def get_top_n_scores_per_category(row):
grouped = row.groupby(category_mapping, axis=0)
return grouped.apply(lambda x: x.nlargest(N).index)
top_n_scores_indices = coolness_score.apply(get_top_n_scores_per_category, axis=1)
Это возвращает N лучших участников в каждой категории на каждый день.
Сценарий 3: Лучшие результаты в каждой категории с ограничениями Теперь давайте реализуем самый сложный сценарий, в котором у вас есть ограничения, определенные с помощью Category_limits.
def get_top_scores_with_limits(row):
grouped = row.groupby(category_mapping, axis=0)
top_scores = []
for category, group in grouped:
limit = category_limits[category]
top_scores.extend(group.nlargest(limit).index)
return top_scores
top_scores_with_limits_indices = coolness_score.apply(get_top_scores_with_limits, axis=1)
Это даст вам индексы участников, соответствующих критериям, основанным на ограничениях категорий.
Обнуление очков проигравших Чтобы обнулить баллы проигравших (участников, не соответствующих критериям):
def zero_out_losers(row):
top_scores = get_top_scores_with_limits(row)
return row.where(row.index.isin(top_scores), 0)
final_scores = coolness_score.apply(zero_out_losers, axis=1)
Это возвращает DataFrame, в котором остаются только самые высокие оценки, а все остальные оценки устанавливаются на ноль.
Дайте мне знать, если это сработает для вас.
Вы можете использовать сопоставление и groupby.rank , затем маску с , где:
# names to categories
cat = coolness_score.columns.map(category_mapping)
# categories to limits
limit = cat.map(category_limits)
# rank per category
rank = coolness_score.T.groupby(cat).rank(method='dense', ascending=False).T
# identify top N per category per row
mask = rank.le(limit)
# mask losers
out = coolness_score.where(mask, 0)
Выход:
Alligator Beryl Chupacabra Dandelion Eggplant Feldspar
2024-08-01 0.000000 0.878593 0.957980 0.887114 0.266656 0.000000
2024-08-02 0.000000 0.660319 0.737451 0.921197 0.446438 0.000000
2024-08-03 0.000000 0.000000 0.765396 0.334504 0.250021 0.736392
2024-08-04 0.000000 0.000000 0.990308 0.357501 0.124491 0.941783
2024-08-05 0.327078 0.000000 0.000000 0.309475 0.538202 0.952041
2024-08-06 0.533576 0.000000 0.000000 0.935781 0.587427 0.690166
2024-08-07 0.767592 0.000000 0.000000 0.222281 0.879662 0.821808
Промежуточные продукты:
# cat
Index(['Animal', 'Mineral', 'Animal', 'Vegetable', 'Vegetable', 'Mineral'], dtype='object')
# limit
Index([1, 1, 1, 2, 2, 1], dtype='int64')
# rank
Alligator Beryl Chupacabra Dandelion Eggplant Feldspar
2024-08-01 2.0 1.0 1.0 1.0 2.0 2.0
2024-08-02 2.0 1.0 1.0 1.0 2.0 2.0
2024-08-03 2.0 2.0 1.0 1.0 2.0 1.0
2024-08-04 2.0 2.0 1.0 1.0 2.0 1.0
2024-08-05 1.0 2.0 2.0 2.0 1.0 1.0
2024-08-06 1.0 2.0 2.0 1.0 2.0 1.0
2024-08-07 1.0 2.0 2.0 2.0 1.0 1.0
# mask
Alligator Beryl Chupacabra Dandelion Eggplant Feldspar
2024-08-01 False True True True True False
2024-08-02 False True True True True False
2024-08-03 False False True True True True
2024-08-04 False False True True True True
2024-08-05 True False False True True True
2024-08-06 True False False True True True
2024-08-07 True False False True True True