Перегруппировать значения столбцов в pandas df
У меня есть script
, который присваивает значение на основе двух columns
в pandas
df
. Код ниже может реализовать первый шаг, но я борюсь со вторым.
Таким образом, скрипт должен изначально:
1) Назначьте Person
для каждого отдельного string
в [Area]
и первого 3 unique values
в [Place]
2) Посмотрите, чтобы переназначить People
менее чем 3 unique values
Пример. df
ниже имеет 6 unique values
в [Area]
и [Place]
. Но 3 People
назначены. В идеале, 2
люди будут 2 unique values
каждый
d = ({
'Time' : ['8:03:00','8:17:00','8:20:00','10:15:00','10:15:00','11:48:00','12:00:00','12:10:00'],
'Place' : ['House 1','House 2','House 1','House 3','House 4','House 5','House 1','House 1'],
'Area' : ['X','X','Y','X','X','X','X','X'],
})
df = pd.DataFrame(data=d)
def g(gps):
s = gps['Place'].unique()
d = dict(zip(s, np.arange(len(s)) // 3 + 1))
gps['Person'] = gps['Place'].map(d)
return gps
df = df.groupby('Area', sort=False).apply(g)
s = df['Person'].astype(str) + df['Area']
df['Person'] = pd.Series(pd.factorize(s)[0] + 1).map(str).radd('Person ')
Выход:
Time Place Area Person
0 8:03:00 House 1 X Person 1
1 8:17:00 House 2 X Person 1
2 8:20:00 House 1 Y Person 2
3 10:15:00 House 3 X Person 1
4 10:15:00 House 4 X Person 3
5 11:48:00 House 5 X Person 3
6 12:00:00 House 1 X Person 1
7 12:10:00 House 1 X Person 1
Как видите, первый шаг работает нормально. или каждый отдельный string
в [Area]
, первый 3 unique values
в [Place]
назначается на Person
. Это оставляет Person 1
с 3 values
, Person 2
с 1 value
и Person 3
с 2 values
.
Второй шаг, где я борюсь.
Если Person
назначено меньше, чем 3 unique values
, измените это так, чтобы у каждого Person
было до 3 unique values
Предполагаемый результат:
Time Place Area Person
0 8:03:00 House 1 X Person 1
1 8:17:00 House 2 X Person 1
2 8:20:00 House 1 Y Person 2
3 10:15:00 House 3 X Person 1
4 10:15:00 House 4 X Person 2
5 11:48:00 House 5 X Person 2
6 12:00:00 House 1 X Person 1
7 12:10:00 House 1 X Person 1
Ответы
Ответ 1
Насколько я понимаю, вы довольны всем до распределения Лица. Итак, вот решение plug and play для "слияния" лиц с менее чем тремя уникальными значениями, поэтому каждый человек получает 3 уникальных значения, за исключением последнего, очевидно (на основе второго и последнего df вы отправили ("Вывод:") без касаясь тех, у которых уже есть 3 уникальных значения, и просто объединяет остальные.
EDIT: значительно упрощен код. Опять же, беря ваш df в качестве входных данных:
n = 3
df['complete'] = df.Person.apply(lambda x: 1 if df.Person.tolist().count(x) == n else 0)
df['num'] = df.Person.str.replace('Person ','')
df.sort_values(by=['num','complete'],ascending=True,inplace=True) #get all persons that are complete to the top
c = 0
person_numbers = []
for x in range(0,999): #Create the numbering [1,1,1,2,2,2,3,3,3,...] with n defining how often a person is 'repeated'
if x % n == 0:
c += 1
person_numbers.append(c)
df['Person_new'] = person_numbers[0:len(df)] #Add the numbering to the df
df.Person = 'Person ' + df.Person_new.astype(str) #Fill the person column with the new numbering
df.drop(['complete','Person_new','num'],axis=1,inplace=True)
Ответ 2
текущая попытка
В следующем я добавил несколько строк до последних строк вашего кода:
d = ({'Time': ['8:03:00', '8:17:00', '8:20:00', '10:15:00', '10:15:00', '11:48:00', '12:00:00', '12:10:00'],
'Place': ['House 1', 'House 2', 'House 1', 'House 3', 'House 4', 'House 5', 'House 1', 'House 1'],
'Area': ['X', 'X', 'Y', 'X', 'X', 'X', 'X', 'X']})
df = pd.DataFrame(data=d)
def g(gps):
s = gps['Place'].unique()
d = dict(zip(s, np.arange(len(s)) // 3 + 1))
gps['Person'] = gps['Place'].map(d)
return gps
df = df.groupby('Area', sort=False).apply(g)
s = df['Person'].astype(str) + df['Area']
# added lines
t = s.value_counts()
df_sub = df.loc[s[s.isin(t[t < 3].index)].index].copy()
df_sub["tag"] = df_sub["Place"] + df_sub["Area"]
tags = list(df_sub.tag.unique())
f = lambda x: f'R{int(tags.index(x) / 3) + 1}'
df_sub['reassign'] = df_sub.tag.apply(f)
s[s.isin(t[t < 3].index)] = df_sub['reassign']
df['Person'] = pd.Series(pd.factorize(s)[0] + 1).map(str).radd('Person ')
Честно говоря, я не уверен, что он работает во всех случаях, но он дает ваш намеченный результат в тестовом примере.
Предыдущие попытки
Посмотрим, смогу ли я помочь с ограниченным пониманием того, что вы пытаетесь сделать.
У вас есть последовательные данные (я буду называть их событиями), и вы хотите назначить каждому событию идентификатор "person". Идентификатор, который вы будете назначать на каждом последующем событии, зависит от предыдущих назначений, и мне кажется, что он должен регулироваться следующими правилами, которые будут применяться последовательно:
-
Я знаю вас: я могу повторно использовать предыдущий идентификатор, если: для данного идентификатора уже появились одни и те же значения для "Place" и "Area" (что-то с этим связано?).
-
Я НЕ знаю вас: я создам новый идентификатор, если: появляется новое значение Area (так что Place and Area играют разные роли?).
-
я вас знаю? : Я могу повторно использовать ранее использованный идентификатор, если: идентификатор не был назначен как минимум для трех событий (что, если это происходит для нескольких идентификаторов? Предполагаю, что я использую самый старый...).
-
nah, я не делаю: если ни одно из предыдущих правил не применимо, я создам новый идентификатор.
Принимая во внимание вышеизложенное, реализуется решение:
# dict of list of past events assigned to each person. key is person identifier
people = dict()
# new column for df (as list) it will be appended at the end to dataframe
persons = list()
# first we define the rules
def i_know_you(people, now):
def conditions(now, past):
return [e for e in past if (now.Place == e.Place) and (now.Area == e.Area)]
i_do = [person for person, past in people.items() if conditions(now, past)]
if i_do:
return i_do[0]
return False
def i_do_not_know_you(people, now):
conditions = not bool([e for past in people.values() for e in past if e.Area == now.Area])
if conditions:
return f'Person {len(people) + 1}'
return False
def do_i_know_you(people, now):
i_do = [person for person, past in people.items() if len(past) < 3]
if i_do:
return i_do[0]
return False
# then we process the sequential data
for event in df.itertuples():
print('event:', event)
for rule in [i_know_you, i_do_not_know_you, do_i_know_you]:
person = rule(people, event)
print('\t', rule.__name__, person)
if person:
break
if not person:
person = f'Person {len(people) + 1}'
print('\t', "nah, I don't", person)
if person in people:
people[person].append(event)
else:
people[person] = [event]
persons.append(person)
df['Person'] = persons
Выход:
event: Pandas(Index=0, Time='8:00:00', Place='House 1', Area='X', Person='Person 1')
i_know_you False
i_do_not_know_you Person 1
event: Pandas(Index=1, Time='8:30:00', Place='House 2', Area='X', Person='Person 1')
i_know_you False
i_do_not_know_you False
do_i_know_you Person 1
event: Pandas(Index=2, Time='9:00:00', Place='House 1', Area='Y', Person='Person 2')
i_know_you False
i_do_not_know_you Person 2
event: Pandas(Index=3, Time='9:30:00', Place='House 3', Area='X', Person='Person 1')
i_know_you False
i_do_not_know_you False
do_i_know_you Person 1
event: Pandas(Index=4, Time='10:00:00', Place='House 4', Area='X', Person='Person 2')
i_know_you False
i_do_not_know_you False
do_i_know_you Person 2
event: Pandas(Index=5, Time='10:30:00', Place='House 5', Area='X', Person='Person 2')
i_know_you False
i_do_not_know_you False
do_i_know_you Person 2
event: Pandas(Index=6, Time='11:00:00', Place='House 1', Area='X', Person='Person 1')
i_know_you Person 1
event: Pandas(Index=7, Time='11:30:00', Place='House 6', Area='X', Person='Person 3')
i_know_you False
i_do_not_know_you False
do_i_know_you False
nah, I don't Person 3
event: Pandas(Index=8, Time='12:00:00', Place='House 7', Area='X', Person='Person 3')
i_know_you False
i_do_not_know_you False
do_i_know_you Person 3
event: Pandas(Index=9, Time='12:30:00', Place='House 8', Area='X', Person='Person 3')
i_know_you False
i_do_not_know_you False
do_i_know_you Person 3
и конечный кадр данных, как вы хотите:
Time Place Area Person
0 8:00:00 House 1 X Person 1
1 8:30:00 House 2 X Person 1
2 9:00:00 House 1 Y Person 2
3 9:30:00 House 3 X Person 1
4 10:00:00 House 4 X Person 2
5 10:30:00 House 5 X Person 2
6 11:00:00 House 1 X Person 1
7 11:30:00 House 6 X Person 3
8 12:00:00 House 7 X Person 3
9 12:30:00 House 8 X Person 3
Примечание. Обратите внимание, что я намеренно избегал использования группированных операций и обработанных данных последовательно. Я думаю, что такая сложность (и не совсем понимание того, что вы хотите сделать...) требует такого подхода. Кроме того, вы можете адаптировать правила, чтобы быть более сложными (время действительно играет роль или нет?), Используя ту же структуру выше.
Обновленный ответ для новых данных
Глядя на новые данные, очевидно, что я не понял, что вы пытаетесь сделать (в частности, назначение не похоже на последовательные правила). У меня было бы решение, которое будет работать на вашем втором наборе данных, но это даст другой результат для первого набора данных.
Решение намного проще и добавит столбец (который вы можете отбросить позже, если хотите):
df["tag"] = df["Place"] + df["Area"]
tags = list(df.tag.unique())
f = lambda x: f'Person {int(tags.index(x) / 3) + 1}'
df['Person'] = df.tag.apply(f)
На втором наборе данных это даст:
Time Place Area tag Person
0 8:00:00 House 1 X House 1X Person 1
1 8:30:00 House 2 X House 2X Person 1
2 9:00:00 House 3 X House 3X Person 1
3 9:30:00 House 1 Y House 1Y Person 2
4 10:00:00 House 1 Z House 1Z Person 2
5 10:30:00 House 1 V House 1V Person 2
На первом наборе данных он дает:
Time Place Area tag Person
0 8:00:00 House 1 X House 1X Person 1
1 8:30:00 House 2 X House 2X Person 1
2 9:00:00 House 1 Y House 1Y Person 1
3 9:30:00 House 3 X House 3X Person 2
4 10:00:00 House 4 X House 4X Person 2
5 10:30:00 House 5 X House 5X Person 2
6 11:00:00 House 1 X House 1X Person 1
7 11:30:00 House 6 X House 6X Person 3
8 12:00:00 House 7 X House 7X Person 3
9 12:30:00 House 8 X House 8X Person 3
Это отличается от вашего предполагаемого результата по индексу 2 и 3. Является ли этот вывод прекрасным с вашим требованием? Почему бы и нет?
Ответ 3
Во-первых, этот ответ не соответствует вашему требованию только переназначить остатки (поэтому я не ожидаю, что вы его примете). Тем не менее, я публикую его в любом случае, потому что ограничение временного окна было сложно решить в мире панд. Возможно, мое решение не будет полезно для вас прямо сейчас, но может быть позже;) По крайней мере, для меня это был опыт обучения, поэтому, возможно, другие могут получить от него.
import pandas as pd
from datetime import datetime, time, timedelta
import random
# --- helper functions for demo
random.seed( 0 )
def makeRandomTimes( nHours = None, mMinutes = None ):
nHours = 10 if nHours is None else nHours
mMinutes = 3 if mMinutes is None else mMinutes
times = []
for _ in range(nHours):
hour = random.randint(8,18)
for _ in range(mMinutes):
minute = random.randint(0,59)
times.append( datetime.combine( datetime.today(), time( hour, minute ) ) )
return times
def makeDf():
times = makeRandomTimes()
houses = [ str(random.randint(1,10)) for _ in range(30) ]
areas = [ ['X','Y'][random.randint(0,1)] for _ in range(30) ]
df = pd.DataFrame( {'Time' : times, 'House' : houses, 'Area' : areas } )
return df.set_index( 'Time' ).sort_index()
# --- real code begins
def evaluateLookback( df, idx, dfg ):
mask = df.index >= dfg.Lookback.iat[-1]
personTotals = df[ mask ].set_index('Loc')['Person'].value_counts()
currentPeople = set(df.Person[ df.Person > -1 ])
noAllocations = currentPeople - set(personTotals.index)
available = personTotals < 3
if noAllocations or available.sum():
# allocate to first available person
person = min( noAllocations.union(personTotals[ available ].index) )
else:
# allocate new person
person = len( currentPeople )
df.Person.at[ idx ] = person
# debug
df.Verbose.at[ idx ] = ( noAllocations, available.sum() )
def lambdaProxy( df, colName ):
[ dff[1][colName].apply( lambda f: f(df,*dff) ) for dff in df.groupby(df.index) ]
lookback = timedelta( minutes = 120 )
df1 = makeDf()
df1[ 'Loc' ] = df1[ 'House' ] + df1[ 'Area' ]
df1[ 'Person' ] = None
df1[ 'Lambda' ] = evaluateLookback
df1[ 'Lookback' ] = df1.index - lookback
df1[ 'Verbose' ] = None
lambdaProxy( df1, 'Lambda' )
print( df1[ [ col for col in df1.columns if col != 'Lambda' ] ] )
И образец вывода на моей машине выглядит так:
House Area Loc Person Lookback Verbose
Time
2018-09-30 08:16:00 6 Y 6Y 0 2018-09-30 06:16:00 ({}, 0)
2018-09-30 08:31:00 4 Y 4Y 0 2018-09-30 06:31:00 ({}, 1)
2018-09-30 08:32:00 10 X 10X 0 2018-09-30 06:32:00 ({}, 1)
2018-09-30 09:04:00 4 X 4X 1 2018-09-30 07:04:00 ({}, 0)
2018-09-30 09:46:00 10 X 10X 1 2018-09-30 07:46:00 ({}, 1)
2018-09-30 09:57:00 4 X 4X 1 2018-09-30 07:57:00 ({}, 1)
2018-09-30 10:06:00 1 Y 1Y 2 2018-09-30 08:06:00 ({}, 0)
2018-09-30 10:39:00 10 X 10X 0 2018-09-30 08:39:00 ({0}, 1)
2018-09-30 10:48:00 7 X 7X 0 2018-09-30 08:48:00 ({}, 2)
2018-09-30 11:08:00 1 Y 1Y 0 2018-09-30 09:08:00 ({}, 3)
2018-09-30 11:18:00 2 Y 2Y 1 2018-09-30 09:18:00 ({}, 2)
2018-09-30 11:32:00 9 X 9X 2 2018-09-30 09:32:00 ({}, 1)
2018-09-30 12:22:00 5 Y 5Y 1 2018-09-30 10:22:00 ({}, 2)
2018-09-30 12:30:00 9 X 9X 1 2018-09-30 10:30:00 ({}, 2)
2018-09-30 12:34:00 6 X 6X 2 2018-09-30 10:34:00 ({}, 1)
2018-09-30 12:37:00 1 Y 1Y 2 2018-09-30 10:37:00 ({}, 1)
2018-09-30 12:45:00 4 X 4X 0 2018-09-30 10:45:00 ({}, 1)
2018-09-30 12:58:00 8 X 8X 0 2018-09-30 10:58:00 ({}, 1)
2018-09-30 14:26:00 7 Y 7Y 0 2018-09-30 12:26:00 ({}, 3)
2018-09-30 14:48:00 2 X 2X 0 2018-09-30 12:48:00 ({1, 2}, 1)
2018-09-30 14:50:00 8 X 8X 1 2018-09-30 12:50:00 ({1, 2}, 0)
2018-09-30 14:53:00 8 Y 8Y 1 2018-09-30 12:53:00 ({2}, 1)
2018-09-30 14:56:00 6 X 6X 1 2018-09-30 12:56:00 ({2}, 1)
2018-09-30 14:58:00 9 Y 9Y 2 2018-09-30 12:58:00 ({2}, 0)
2018-09-30 17:09:00 2 Y 2Y 0 2018-09-30 15:09:00 ({0, 1, 2}, 0)
2018-09-30 17:19:00 4 X 4X 0 2018-09-30 15:19:00 ({1, 2}, 1)
2018-09-30 17:57:00 6 Y 6Y 0 2018-09-30 15:57:00 ({1, 2}, 1)
2018-09-30 18:21:00 3 X 3X 1 2018-09-30 16:21:00 ({1, 2}, 0)
2018-09-30 18:30:00 9 X 9X 1 2018-09-30 16:30:00 ({2}, 1)
2018-09-30 18:35:00 8 Y 8Y 1 2018-09-30 16:35:00 ({2}, 1)
>>>
Заметки:
- переменная
lookback
контролирует время, обратное назад, чтобы рассмотреть местоположения, выделенные человеку - столбец
Lookback
показывает время отсечки -
evaluateLookback
вызывается многократно для каждой строки в таблице, а df
- это весь DataFrame, idx
текущий индекс/метка и dfg
текущей строки. -
lambdaProxy
контролирует призвание evaluateLookback
. - количество мест на человека установлено равным
3
но может быть скорректировано по мере необходимости - сколь угодно сложные требования к периоду ретроспективного анализа можно управлять путем иметь еще один столбец Func который сначала оценивали по
lambdaProxy
, а затем, что результат сохранить и использовать в течение evaluateLookback
В демо-версии есть несколько интересных случаев: 10:39:00
, 14:48:00
, 17:09:00
Помимо этого: Было бы интересно увидеть "столбцы функций" в пандах, возможно, с возможностью запоминания? В идеале столбец "Человек" должен выполнять функцию и вычислять по запросу либо со своей собственной строкой, либо с некоторым видом окна с переменным окном. Кто-нибудь видел что-то подобное?
Ответ 4
Как насчет этого для шага 2:
def reduce_df(df):
values = df['Area'] + df['Place']
df1 = df.loc[~values.duplicated(),:] # ignore duplicate values for this part..
person_count = df1.groupby('Person')['Person'].agg('count')
leftover_count = person_count[person_count < 3] # the 'leftovers'
# try merging pairs together
nleft = leftover_count.shape[0]
to_try = np.arange(nleft - 1)
to_merge = (leftover_count.values[to_try] +
leftover_count.values[to_try + 1]) <= 3
to_merge[1:] = to_merge[1:] & ~to_merge[:-1]
to_merge = to_try[to_merge]
merge_dict = dict(zip(leftover_count.index.values[to_merge+1],
leftover_count.index.values[to_merge]))
def change_person(p):
if p in merge_dict.keys():
return merge_dict[p]
return p
reduced_df = df.copy()
# update df with the merges you found
reduced_df['Person'] = reduced_df['Person'].apply(change_person)
return reduced_df
print(
reduce_df(reduce_df(df)) # call twice in case 1,1,1 -> 2,1 -> 3
)
Выход:
Area Place Time Person
0 X House 1 8:03:00 Person 1
1 X House 2 8:17:00 Person 1
2 Y House 1 8:20:00 Person 2
3 X House 3 10:15:00 Person 1
4 X House 4 10:15:00 Person 2
5 X House 5 11:48:00 Person 2
6 X House 1 12:00:00 Person 1
7 X House 1 12:10:00 Person 1