Перегруппировать значения столбцов в Panda df
У меня есть script
, который присваивает значение, основанное на двух columns
в a pandas
df
. Приведенный ниже код способен реализовать 1-й шаг, но я борюсь со вторым.
Поэтому сценарий должен изначально:
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
.Второй шаг - это когда я борюсь.
Если a 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
Описание:
Person 1
уже было назначено всем добрым. Person 2
и 3
имели меньше, поэтому мы должны искать, чтобы объединить их. Все повторяющиеся значения должны оставаться одинаковыми.
4 ответов:
Насколько я понимаю, вы довольны всем до выделения человека. Итак, вот решение plug and play для "слияния"людей с менее чем 3 уникальными значениями, чтобы каждый человек заканчивал с 3 уникальными значениями, за исключением последнего, очевидно (на основе предпоследнего df, который вы опубликовали ("Output:"), не касаясь тех, которые уже имеют 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)
Текущая попытка
В следующем я добавил несколько строк перед последними строками вашего кода:
Честно говоря, я не уверен, что он работает во всех случаях, но он дает желаемый результат в тестовом случае.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 ')
Предыдущие попытки
Давайте посмотрим, смогу ли я помочь с ограниченным пониманием того, что вы пытаетесь сделать.У вас есть последовательные данные (я буду называть их событиями), и вы хотите назначить каждому событию идентификатор "человека". Идентификатор, который вы будете присваивать каждому последующему событию, зависит от предыдущих назначений, и мне кажется, что он должен управляться следующими правилами, которые будут применяться последовательно:
Я знаю вас : я могу повторно использовать предыдущий идентификатор, если: те же значения для "места " и" площади " уже появились для данного идентификатора (имеет ли время что-то с этим делать?).
Я не знаю вас : я создам новый идентификатор, если: новое значение Появляется область (так место и область играют разные роли?).
Я тебя знаю?: я мог бы повторно использовать ранее используемый идентификатор, если: идентификатор не был назначен по крайней мере трем событиям (что, если это произойдет для нескольких идентификаторов?Я предположу, что использую самый старый...).
Нет, я не : в случае, если ни одно из предыдущих правил не применяется, Я создам новый идентификатор.
Приняв вышесказанное ниже приводится реализация решения:
# 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. Этот результат соответствует вашим требованиям? А почему бы и нет?
Во-первых, этот ответ не соответствует вашему требованию только переназначить остатки (поэтому я не ожидаю, что вы его примете). Тем не менее, я публикую его в любом случае, потому что ваше ограничение временного окна было сложно решить в мире панд. Возможно, мое решение не будет полезным для вас прямо сейчас, но, возможно, позже ;) по крайней мере, это был опыт обучения для меня - так что, возможно, другие могут извлечь из него пользу.
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
является целым фреймом данных,idx
текущим индексом/меткой иdfg
текущей строкой.lambdaProxy
управляет вызовомevaluateLookback
.- число локаций на человека устанавливается равным
3
, но это может быть скорректировано по мере необходимости- сколь угодно сложные требования для периода ретроспективного анализа могут управляться с помощью другого столбца func, который сначала оценивается
lambdaProxy
, а затем этот результат сохраняется и используется вevaluateLookback
В демо-выводе есть несколько интересных крайних случаев:
10:39:00
,14:48:00
,17:09:00
В сторону: было бы интересно увидеть "колонку функций"у панд, возможно, с памятью, подобной способности? В идеале колонка "персона" должна занимать функция и calc по запросу, либо со своей собственной строкой, либо с некоторым переменным видом окна. Кто-нибудь видел что-то подобное?
Как насчет этого для шага 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