Перегруппировать значения столбцов в 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 имели меньше, поэтому мы должны искать, чтобы объединить их. Все повторяющиеся значения должны оставаться одинаковыми.

308   4  

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 ')
Честно говоря, я не уверен, что он работает во всех случаях, но он дает желаемый результат в тестовом случае.

Предыдущие попытки

Давайте посмотрим, смогу ли я помочь с ограниченным пониманием того, что вы пытаетесь сделать.

У вас есть последовательные данные (я буду называть их событиями), и вы хотите назначить каждому событию идентификатор "человека". Идентификатор, который вы будете присваивать каждому последующему событию, зависит от предыдущих назначений, и мне кажется, что он должен управляться следующими правилами, которые будут применяться последовательно:

  1. Я знаю вас : я могу повторно использовать предыдущий идентификатор, если: те же значения для "места " и" площади " уже появились для данного идентификатора (имеет ли время что-то с этим делать?).

  2. Я не знаю вас : я создам новый идентификатор, если: новое значение Появляется область (так место и область играют разные роли?).

  3. Я тебя знаю?: я мог бы повторно использовать ранее используемый идентификатор, если: идентификатор не был назначен по крайней мере трем событиям (что, если это произойдет для нескольких идентификаторов?Я предположу, что использую самый старый...).

  4. Нет, я не : в случае, если ни одно из предыдущих правил не применяется, Я создам новый идентификатор.

Приняв вышесказанное ниже приводится реализация решения:

# 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
    Ничего не найдено.

Добавить ответ:
Отменить.