python - tablas - seleccionar columnas de un dataframe pandas
Reagrupar valores de columna en una pandas df (4)
intento actual
A continuación, he agregado algunas líneas antes de las últimas líneas de su código:
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 '')
Para ser honesto, no estoy tan seguro de que funcione en todos los casos, pero proporciona el resultado deseado en el caso de prueba.
Intentos anteriores
Veamos si puedo ayudarlo con una comprensión limitada de lo que intenta hacer.
Tiene datos secuenciales (los llamaré eventos) y desea asignar a cada evento un identificador de "persona". El identificador que asignará en cada suceso sucesivo depende de las asignaciones anteriores y me parece que debe regirse por las siguientes reglas que se deben aplicar de forma secuencial:
Te conozco : puedo reutilizar un identificador anterior si: ya aparecieron los mismos valores para "Lugar" y "Área" para un identificador dado (¿ tiene tiempo algo que ver con eso? ).
NO te conozco : crearé un nuevo identificador si: aparece un nuevo valor de Área (¿ así que Lugar y Área desempeñan roles diferentes? ).
¿te conozco? : Podría reutilizar un identificador utilizado anteriormente si: no se ha asignado un identificador a al menos tres eventos ( ¿qué sucede si esto ocurre con varios identificadores? Asumiré que uso el más antiguo ...).
nah, no lo hago : en caso de que ninguna de las reglas anteriores se aplique, crearé un nuevo identificador.
Habiendo asumido lo anterior, lo siguiente es una implementación de una solución:
# 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
Salida:
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
y el último marco de datos es, como quieras:
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
Observación : tenga en cuenta que evité intencionalmente el uso agrupado por operaciones y procesé datos de forma secuencial. Creo que este tipo de complejidad ( y no entender realmente lo que quieres hacer ... ) requiere ese enfoque. Además, puede adaptar las reglas para que sean más complicadas ( ¿el tiempo realmente juega un papel o no? ) Utilizando la misma estructura anterior.
Respuesta actualizada para nuevos datos.
Al observar los nuevos datos es evidente que no entendí lo que está tratando de hacer (en particular, la asignación no parece seguir reglas secuenciales ). Tendría una solución que funcionaría en su segundo conjunto de datos, pero daría un resultado diferente para el primer conjunto de datos.
La solución es mucho más simple y agregará una columna (que puede soltar más adelante si lo desea):
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)
En el segundo conjunto de datos, daría:
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
En el primer conjunto de datos da:
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
Esto es diferente de su salida prevista en el índice 2 y 3. ¿Esta salida está bien con sus requisitos? Por qué no?
Tengo un script
que asigna un valor basado en dos columns
en una pandas
df
. El código de abajo es capaz de implementar el primer paso, pero estoy luchando con el segundo.
Así que el guión debería inicialmente:
1) Asigne una Person
para cada string
individual en [Area]
y los primeros 3 unique values
en [Place]
2) Busque reasignar People
con menos de 3 unique values
. Ejemplo. El df
continuación tiene 6 unique values
en [Area]
y [Place]
. Pero se asignan 3 People
. Idealmente, 2
personas tendrán 2 unique values
cada una.
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 '')
Salida:
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
Como puedes ver, el primer paso funciona bien. o cada string
individual en [Area]
, los primeros 3 unique values
en [Place]
se asignan a una Person
. Esto deja a la Person 1
con 3 values
, la Person 2
con 1 value
y la Person 3
con 2 values
.
El segundo paso es donde estoy luchando.
Si una Person
tiene menos de 3 unique values
asignados, modifíquelo para que cada Person
tenga hasta 3 unique values
Salida prevista:
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
Descripción:
Person 1
ya tenía 3 unique values
asignados a todo lo bueno. Person 2
y 3
tenían menos, así que deberíamos buscar combinar estos. Todos los valores duplicados deben seguir siendo los mismos.
¿Qué tal esto para el paso 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
)
La salida:
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
En primer lugar, esta respuesta no cumple con su requisito de reasignar únicamente las sobras (por lo que no espero que las acepte). Dicho esto, lo estoy publicando de todos modos porque su restricción de ventana de tiempo fue difícil de resolver dentro de un mundo pandas. Quizás mi solución no sea útil para usted en este momento, pero tal vez más adelante;) Al menos fue una experiencia de aprendizaje para mí, por lo que tal vez otros puedan beneficiarse de ella.
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'' ] ] )
Y la salida de muestra en mi máquina se ve así:
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)
>>>
Notas:
- la variable
lookback
controla la cantidad de tiempo mirando hacia atrás para considerar las ubicaciones asignadas a una persona - la columna
Lookback
muestra el tiempo de corte -
evaluateLookback
se llama repetidamente para cada fila de la tabla, condf
siendo el DataFrame completo,idx
el índice / etiqueta actual ydfg
la fila actual. -
lambdaProxy
controla la llamada delambdaProxy
. - el número de ubicaciones por persona se establece en
3
pero eso podría ajustarse según sea necesario - los requisitos arbitrariamente complejos para el período de lookback se pueden administrar al tener otra columna de función que primero se evalúa con
lambdaProxy
y luego ese resultado se almacena y se usa dentro delambdaProxy
Hay algunos casos interesantes en la salida de demostración: 10:39:00
, 14:48:00
, 17:09:00
Aparte: ¿Sería interesante ver la "columna de funciones" en los pandas, tal vez con capacidad de memorización? Lo ideal es que la columna ''Persona'' tome una función y calcule a petición, ya sea con su propia fila o con alguna vista de ventana variable. ¿Alguien ha visto algo así?
Por lo que entiendo, estás contento con todo antes de la asignación de persona. Así que aquí hay una solución plug and play para "fusionar" Personas con menos de 3 valores únicos, de modo que cada Persona termine con 3 valores únicos excepto el último (obviamente, en función del segundo hasta el último df que publicó ("Salida:") sin Tocando los que ya tienen 3 valores únicos y simplemente fusiona los otros.
EDITAR: Código muy simplificado. De nuevo, tomando su df como entrada:
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)