python - tutorial - recorrer data frame pandas
Reasignar valores de columna en un pandas df (3)
Esta pregunta está relacionada con las listas o el personal. Estoy tratando de asignar varios trabajos a individuos (empleados). Usando el df
continuación,
`[Person]` = Individuals (employees)
`[Area]` and `[Place]` = unique jobs
`[On]` = How many unique jobs are occurring at each point in time
Así que [Area]
y [Place]
juntos conformarán valores unique
que son trabajos diferentes. Estos valores se asignarán a individuos con el objetivo general de utilizar la menor cantidad posible de individuos. El valor más exclusivo assigned
a cualquier individuo es 3. [On]
muestra cuántos valores unique
actuales para [Place]
y [Area]
están ocurriendo. Así que esto proporciona una guía concreta sobre cuántas personas necesito. Por ejemplo,
1-3 unique values occurring = 1 individual
4-6 unique values occurring = 2 individuals
7-9 unique values occurring = 3 individuals etc
Pregunta: Donde la cantidad de valores unique
en [Area]
y [Place]
es mayor que 3 me está causando problemas. No puedo hacer un grupo groupby
cual assign
los primeros 3 unique values
al individual 1
y los siguientes 3 valores unique
al individual 2
etc. Quiero agrupar valores únicos en [Area]
y [Place]
por [Area]
. Así que busque assign
mismos valores en [Area]
a un individuo (hasta 3). Luego, si hay valores remanentes (<3), deben combinarse para formar un grupo de 3, cuando sea posible.
La forma en que me imagino este trabajo es: mira el futuro por una hour
. Para cada nueva row
de valores, el script
debe ver cuántos valores estarán [On]
(esto proporciona una indicación de cuántos individuos se requieren en total). Cuando los valores unique
son> 3, deben assigned
grouping
el mismo valor en [Area]
. Si hay valores remanentes , deben combinarse de todos modos para formar un grupo de 3.
Poner eso en un proceso paso a paso:
1) Use la Column
[On]
para determinar cuántas personas se requieren al mirar hacia el futuro durante una hour
2) Cuando se producen más de 3 valores unique
, asigne primero los valores idénticos en [Area]
.
3) Si hay valores remanentes , busque combinar de todos modos.
Para el df
continuación, hay 9 valores unique
que ocurren para [Place]
y [Area]
con una hour
. Entonces deberíamos tener 3 individuos assigned
. Cuando los valores unique
> 3 deben ser asignados por [Area]
y ver si ocurre el mismo valor. Los valores remanentes deben combinarse con otros individuos que tengan menos de 3 valores unique
.
import pandas as pd
import numpy as np
d = ({
''Time'' : [''8:03:00'',''8:17:00'',''8:20:00'',''8:28:00'',''8:35:00'',''08:40:00'',''08:42:00'',''08:45:00'',''08:50:00''],
''Place'' : [''House 1'',''House 2'',''House 3'',''House 4'',''House 5'',''House 1'',''House 2'',''House 3'',''House 2''],
''Area'' : [''A'',''B'',''C'',''D'',''E'',''D'',''E'',''F'',''G''],
''On'' : [''1'',''2'',''3'',''4'',''5'',''6'',''7'',''8'',''9''],
''Person'' : [''Person 1'',''Person 2'',''Person 3'',''Person 4'',''Person 5'',''Person 4'',''Person 5'',''Person 6'',''Person 7''],
})
df = pd.DataFrame(data=d)
Este es mi intento:
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
df1 = (reduce_df(reduce_df(df)))
Esta es la salida:
Time Place Area On Person
0 8:03:00 House 1 A 1 Person 1
1 8:17:00 House 2 B 2 Person 1
2 8:20:00 House 3 C 3 Person 1
3 8:28:00 House 4 D 4 Person 4
4 8:35:00 House 5 E 5 Person 5
5 8:40:00 House 1 D 6 Person 4
6 8:42:00 House 2 E 7 Person 5
7 8:45:00 House 3 F 8 Person 5
8 8:50:00 House 2 G 9 Person 7
Esta es mi salida prevista:
Time Place Area On Person
0 8:03:00 House 1 A 1 Person 1
1 8:17:00 House 2 B 2 Person 1
2 8:20:00 House 3 C 3 Person 1
3 8:28:00 House 4 D 4 Person 2
4 8:35:00 House 5 E 5 Person 3
5 8:40:00 House 6 D 6 Person 2
6 8:42:00 House 2 E 7 Person 3
7 8:45:00 House 3 F 8 Person 2
8 8:50:00 House 2 G 9 Person 3
Descripción de cómo quiero obtener esta salida:
Index 0: One `unique` value occurring. So `assign` to individual 1
Index 1: Two `unique` values occurring. So `assign` to individual 1
Index 2: Three `unique` values occurring. So `assign` to individual 1
Index 3: Four `unique` values on. So `assign` to individual 2
Index 4: Five `unique` values on. This one is a bit tricky and hard to conceptualise. But there is another `E` within an `hour`. So `assign` to a new individual so it can be combined with the other `E`
Index 5: Six `unique` values on. Should be `assigned` with the other `D`. So individual 2
Index 6: Seven `unique` values on. Should be `assigned` with other `E`. So individual 3
Index 7: Eight `unique` values on. New value in `[Area]`, which is a _leftover_. `Assign` to either individual 2 or 3
Index 8: Nine `unique` values on. New value in `[Area]`, which is a _leftover_. `Assign` to either individual 2 or 3
Ejemplo No2:
d = ({
''Time'' : [''8:03:00'',''8:17:00'',''8:20:00'',''8:28:00'',''8:35:00'',''8:40:00'',''8:42:00'',''8:45:00'',''8:50:00''],
''Place'' : [''House 1'',''House 2'',''House 3'',''House 1'',''House 2'',''House 3'',''House 1'',''House 2'',''House 3''],
''Area'' : [''X'',''X'',''X'',''X'',''X'',''X'',''X'',''X'',''X''],
''On'' : [''1'',''2'',''3'',''3'',''3'',''3'',''3'',''3'',''3''],
''Person'' : [''Person 1'',''Person 1'',''Person 1'',''Person 1'',''Person 1'',''Person 1'',''Person 1'',''Person 1'',''Person 1''],
})
df = pd.DataFrame(data=d)
Estoy recibiendo un error:
IndexError: index 1 is out of bounds for axis 1 with size 1
En esta línea:
df.loc[:,''Person''] = df[''Person''].unique()[assignedPeople]
Sin embargo, si cambio la persona a 1,2,3 repitiendo, devuelve lo siguiente:
''Person'' : [''Person 1'',''Person 2'',''Person 3'',''Person 1'',''Person 2'',''Person 3'',''Person 1'',''Person 2'',''Person 3''],
Time Place Area On Person
0 8:03:00 House 1 X 1 Person 1
1 8:17:00 House 2 X 2 Person 1
2 8:20:00 House 3 X 3 Person 1
3 8:28:00 House 1 X 3 Person 2
4 8:35:00 House 2 X 3 Person 2
5 8:40:00 House 3 X 3 Person 2
6 8:42:00 House 1 X 3 Person 3
7 8:45:00 House 2 X 3 Person 3
8 8:50:00 House 3 X 3 Person 3
Salida prevista:
Time Place Area On Person
0 8:03:00 House 1 X 1 Person 1
1 8:17:00 House 2 X 2 Person 1
2 8:20:00 House 3 X 3 Person 1
3 8:28:00 House 1 X 3 Person 1
4 8:35:00 House 2 X 3 Person 1
5 8:40:00 House 3 X 3 Person 1
6 8:42:00 House 1 X 3 Person 1
7 8:45:00 House 2 X 3 Person 1
8 8:50:00 House 3 X 3 Person 1
La principal conclusión del ejemplo 2 es:
1) There are <3 unique values on so assign to individual 1
Actualizar
Hay una versión en vivo de esta respuesta en línea que puedes probar por ti mismo.
Aquí hay una respuesta en la forma de la función allocatePeople
. Se basa en precomputar todos los índices en los que las áreas se repiten en una hora:
from collections import Counter
import numpy as np
import pandas as pd
def getAssignedPeople(df, areasPerPerson):
areas = df[''Area''].values
places = df[''Place''].values
times = pd.to_datetime(df[''Time'']).values
maxPerson = np.ceil(areas.size / float(areasPerPerson)) - 1
assignmentCount = Counter()
assignedPeople = []
assignedPlaces = {}
heldPeople = {}
heldAreas = {}
holdAvailable = True
person = 0
# search for repeated areas. Mark them if the next repeat occurs within an hour
ixrep = np.argmax(np.triu(areas.reshape(-1, 1)==areas, k=1), axis=1)
holds = np.zeros(areas.size, dtype=bool)
holds[ixrep.nonzero()] = (times[ixrep[ixrep.nonzero()]] - times[ixrep.nonzero()]) < np.timedelta64(1, ''h'')
for area,place,hold in zip(areas, places, holds):
if (area, place) in assignedPlaces:
# this unique (area, place) has already been assigned to someone
assignedPeople.append(assignedPlaces[(area, place)])
continue
if assignmentCount[person] >= areasPerPerson:
# the current person is already assigned to enough areas, move on to the next
a = heldPeople.pop(person, None)
heldAreas.pop(a, None)
person += 1
if area in heldAreas:
# assign to the person held in this area
p = heldAreas.pop(area)
heldPeople.pop(p)
else:
# get the first non-held person. If we need to hold in this area,
# also make sure the person has at least 2 free assignment slots,
# though if it''s the last person assign to them anyway
p = person
while p in heldPeople or (hold and holdAvailable and (areasPerPerson - assignmentCount[p] < 2)) and not p==maxPerson:
p += 1
assignmentCount.update([p])
assignedPlaces[(area, place)] = p
assignedPeople.append(p)
if hold:
if p==maxPerson:
# mark that there are no more people available to perform holds
holdAvailable = False
# this area recurrs in an hour, mark that the person should be held here
heldPeople[p] = area
heldAreas[area] = p
return assignedPeople
def allocatePeople(df, areasPerPerson=3):
assignedPeople = getAssignedPeople(df, areasPerPerson=areasPerPerson)
df = df.copy()
df.loc[:,''Person''] = df[''Person''].unique()[assignedPeople]
return df
Tenga en cuenta el uso de df[''Person''].unique()
en allocatePeople
. Que maneja el caso donde las personas se repiten en la entrada. Se supone que el orden de las personas en la entrada es el orden deseado en el que esas personas deberían ser asignadas.
Probé allocatePeople
contra la entrada de ejemplo del OP ( example1
y example2
) y también contra un par de casos de borde que se me ocurrió que creo que (?) Coinciden con el algoritmo deseado del OP:
ds = dict(
example1 = ({
''Time'' : [''8:03:00'',''8:17:00'',''8:20:00'',''8:28:00'',''8:35:00'',''08:40:00'',''08:42:00'',''08:45:00'',''08:50:00''],
''Place'' : [''House 1'',''House 2'',''House 3'',''House 4'',''House 5'',''House 1'',''House 2'',''House 3'',''House 2''],
''Area'' : [''A'',''B'',''C'',''D'',''E'',''D'',''E'',''F'',''G''],
''On'' : [''1'',''2'',''3'',''4'',''5'',''6'',''7'',''8'',''9''],
''Person'' : [''Person 1'',''Person 2'',''Person 3'',''Person 4'',''Person 5'',''Person 4'',''Person 5'',''Person 6'',''Person 7''],
}),
example2 = ({
''Time'' : [''8:03:00'',''8:17:00'',''8:20:00'',''8:28:00'',''8:35:00'',''8:40:00'',''8:42:00'',''8:45:00'',''8:50:00''],
''Place'' : [''House 1'',''House 2'',''House 3'',''House 1'',''House 2'',''House 3'',''House 1'',''House 2'',''House 3''],
''Area'' : [''X'',''X'',''X'',''X'',''X'',''X'',''X'',''X'',''X''],
''On'' : [''1'',''2'',''3'',''3'',''3'',''3'',''3'',''3'',''3''],
''Person'' : [''Person 1'',''Person 1'',''Person 1'',''Person 1'',''Person 1'',''Person 1'',''Person 1'',''Person 1'',''Person 1''],
}),
long_repeats = ({
''Time'' : [''8:03:00'',''8:17:00'',''8:20:00'',''8:25:00'',''8:30:00'',''8:31:00'',''8:35:00'',''8:45:00'',''8:50:00''],
''Place'' : [''House 1'',''House 2'',''House 3'',''House 4'',''House 1'',''House 1'',''House 2'',''House 3'',''House 2''],
''Area'' : [''A'',''A'',''A'',''A'',''B'',''C'',''C'',''C'',''B''],
''Person'' : [''Person 1'',''Person 1'',''Person 1'',''Person 2'',''Person 3'',''Person 4'',''Person 4'',''Person 4'',''Person 3''],
''On'' : [''1'',''2'',''3'',''4'',''5'',''6'',''7'',''8'',''9''],
}),
many_repeats = ({
''Time'' : [''8:03:00'',''8:17:00'',''8:20:00'',''8:28:00'',''8:35:00'',''08:40:00'',''08:42:00'',''08:45:00'',''08:50:00''],
''Place'' : [''House 1'',''House 2'',''House 3'',''House 4'',''House 1'',''House 1'',''House 2'',''House 1'',''House 2''],
''Area'' : [''A'', ''B'', ''C'', ''D'', ''D'', ''E'', ''E'', ''F'', ''F''],
''On'' : [''1'',''2'',''3'',''4'',''5'',''6'',''7'',''8'',''9''],
''Person'' : [''Person 1'',''Person 1'',''Person 1'',''Person 2'',''Person 3'',''Person 4'',''Person 3'',''Person 5'',''Person 6''],
}),
large_gap = ({
''Time'' : [''8:03:00'',''8:17:00'',''8:20:00'',''8:28:00'',''8:35:00'',''08:40:00'',''08:42:00'',''08:45:00'',''08:50:00''],
''Place'' : [''House 1'',''House 2'',''House 3'',''House 4'',''House 1'',''House 1'',''House 2'',''House 1'',''House 3''],
''Area'' : [''A'', ''B'', ''C'', ''D'', ''E'', ''F'', ''D'', ''D'', ''D''],
''On'' : [''1'',''2'',''3'',''4'',''5'',''6'',''7'',''8'',''9''],
''Person'' : [''Person 1'',''Person 1'',''Person 1'',''Person 2'',''Person 3'',''Person 4'',''Person 3'',''Person 5'',''Person 6''],
}),
different_times = ({
''Time'' : [''8:03:00'',''8:17:00'',''8:20:00'',''8:28:00'',''8:35:00'',''08:40:00'',''09:42:00'',''09:45:00'',''09:50:00''],
''Place'' : [''House 1'',''House 2'',''House 3'',''House 4'',''House 1'',''House 1'',''House 2'',''House 1'',''House 1''],
''Area'' : [''A'', ''B'', ''C'', ''D'', ''D'', ''E'', ''E'', ''F'', ''G''],
''On'' : [''1'',''2'',''3'',''4'',''5'',''6'',''7'',''8'',''9''],
''Person'' : [''Person 1'',''Person 1'',''Person 1'',''Person 2'',''Person 3'',''Person 4'',''Person 3'',''Person 5'',''Person 6''],
})
)
expectedPeoples = dict(
example1 = [1,1,1,2,3,2,3,2,3],
example2 = [1,1,1,1,1,1,1,1,1],
long_repeats = [1,1,1,2,2,3,3,3,2],
many_repeats = [1,1,1,2,2,3,3,2,3],
large_gap = [1,1,1,2,3,3,2,2,3],
different_times = [1,1,1,2,2,2,3,3,3],
)
for name,d in ds.items():
df = pd.DataFrame(d)
expected = [''Person %d'' % i for i in expectedPeoples[name]]
ap = allocatePeople(df)
print(name, ap, sep=''/n'', end=''/n/n'')
np.testing.assert_array_equal(ap[''Person''], expected)
Las declaraciones assert_array_equal
pasan y la salida coincide con la salida esperada de OP:
example1
Time Place Area On Person
0 8:03:00 House 1 A 1 Person 1
1 8:17:00 House 2 B 2 Person 1
2 8:20:00 House 3 C 3 Person 1
3 8:28:00 House 4 D 4 Person 2
4 8:35:00 House 5 E 5 Person 3
5 08:40:00 House 1 D 6 Person 2
6 08:42:00 House 2 E 7 Person 3
7 08:45:00 House 3 F 8 Person 2
8 08:50:00 House 2 G 9 Person 3
example2
Time Place Area On Person
0 8:03:00 House 1 X 1 Person 1
1 8:17:00 House 2 X 2 Person 1
2 8:20:00 House 3 X 3 Person 1
3 8:28:00 House 1 X 3 Person 1
4 8:35:00 House 2 X 3 Person 1
5 8:40:00 House 3 X 3 Person 1
6 8:42:00 House 1 X 3 Person 1
7 8:45:00 House 2 X 3 Person 1
8 8:50:00 House 3 X 3 Person 1
La salida para mis casos de prueba coincide con mis expectativas también:
long_repeats
Time Place Area Person On
0 8:03:00 House 1 A Person 1 1
1 8:17:00 House 2 A Person 1 2
2 8:20:00 House 3 A Person 1 3
3 8:25:00 House 4 A Person 2 4
4 8:30:00 House 1 B Person 2 5
5 8:31:00 House 1 C Person 3 6
6 8:35:00 House 2 C Person 3 7
7 8:45:00 House 3 C Person 3 8
8 8:50:00 House 2 B Person 2 9
many_repeats
Time Place Area On Person
0 8:03:00 House 1 A 1 Person 1
1 8:17:00 House 2 B 2 Person 1
2 8:20:00 House 3 C 3 Person 1
3 8:28:00 House 4 D 4 Person 2
4 8:35:00 House 1 D 5 Person 2
5 08:40:00 House 1 E 6 Person 3
6 08:42:00 House 2 E 7 Person 3
7 08:45:00 House 1 F 8 Person 2
8 08:50:00 House 2 F 9 Person 3
large_gap
Time Place Area On Person
0 8:03:00 House 1 A 1 Person 1
1 8:17:00 House 2 B 2 Person 1
2 8:20:00 House 3 C 3 Person 1
3 8:28:00 House 4 D 4 Person 2
4 8:35:00 House 1 E 5 Person 3
5 08:40:00 House 1 F 6 Person 3
6 08:42:00 House 2 D 7 Person 2
7 08:45:00 House 1 D 8 Person 2
8 08:50:00 House 3 D 9 Person 3
different_times
Time Place Area On Person
0 8:03:00 House 1 A 1 Person 1
1 8:17:00 House 2 B 2 Person 1
2 8:20:00 House 3 C 3 Person 1
3 8:28:00 House 4 D 4 Person 2
4 8:35:00 House 1 D 5 Person 2
5 08:40:00 House 1 E 6 Person 2
6 09:42:00 House 2 E 7 Person 3
7 09:45:00 House 1 F 8 Person 3
8 09:50:00 House 1 G 9 Person 3
Déjame saber si hace todo lo que quieres, o si todavía necesita algunos ajustes. Creo que todos están ansiosos por verte cumplir tu visión.
Al escribir mi otra respuesta , poco a poco me di cuenta de que el algoritmo del OP podría ser más fácil de implementar con un enfoque que se centre en los trabajos (que pueden ser diferentes), en lugar de las personas (que son todas iguales). Aquí hay una solución que utiliza el enfoque centrado en el trabajo:
from collections import Counter
import numpy as np
import pandas as pd
def assignJob(job, assignedix, areasPerPerson):
for i in range(len(assignedix)):
if (areasPerPerson - len(assignedix[i])) >= len(job):
assignedix[i].extend(job)
return True
else:
return False
def allocatePeople(df, areasPerPerson=3):
areas = df[''Area''].values
times = pd.to_datetime(df[''Time'']).values
peopleUniq = df[''Person''].unique()
npeople = int(np.ceil(areas.size / float(areasPerPerson)))
# search for repeated areas. Mark them if the next repeat occurs within an hour
ixrep = np.argmax(np.triu(areas.reshape(-1, 1)==areas, k=1), axis=1)
holds = np.zeros(areas.size, dtype=bool)
holds[ixrep.nonzero()] = (times[ixrep[ixrep.nonzero()]] - times[ixrep.nonzero()]) < np.timedelta64(1, ''h'')
jobs =[]
_jobdict = {}
for i,(area,hold) in enumerate(zip(areas, holds)):
if hold:
_jobdict[area] = job = _jobdict.get(area, []) + [i]
if len(job)==areasPerPerson:
jobs.append(_jobdict.pop(area))
elif area in _jobdict:
jobs.append(_jobdict.pop(area) + [i])
else:
jobs.append([i])
jobs.sort()
assignedix = [[] for i in range(npeople)]
for job in jobs:
if not assignJob(job, assignedix, areasPerPerson):
# break the job up and try again
for subjob in ([sj] for sj in job):
assignJob(subjob, assignedix, areasPerPerson)
df = df.copy()
for i,aix in enumerate(assignedix):
df.loc[aix, ''Person''] = peopleUniq[i]
return df
Esta versión de allocatePeople
también se ha probado ampliamente y pasa todas las mismas comprobaciones descritas en mi otra respuesta.
Tiene más bucles que mi otra solución, por lo que es probable que sea un poco menos eficiente (aunque solo importará si su marco de datos es muy grande, digamos 1e6
filas y más). Por otro lado, es algo más corto y, creo, más sencillo y fácil de entender.
Ok, antes de profundizar en la lógica del problema, vale la pena hacer un poco de limpieza para ordenar los datos y llevarlos a un formato más útil:
#Create table of unique people
unique_people = df[[''Person'']].drop_duplicates().sort_values([''Person'']).reset_index(drop=True)
#Reformat time column
df[''Time''] = pd.to_datetime(df[''Time''])
Ahora, llegando a la lógica del problema, es útil dividir el problema en etapas. En primer lugar, desearemos crear trabajos individuales (con números de trabajo) basados en el "Área" y el tiempo entre ellos. Es decir, trabajos en la misma área, dentro de una hora pueden compartir el mismo número de trabajo.
#Assign jobs
df= df.sort_values([''Area'',''Time'']).reset_index(drop=True)
df[''Job no''] = 0
current_job = 1
df.loc[0,''Job no''] = current_job
for i in range(rows-1):
prev_row = df.loc[i]
row = df.loc[i+1]
time_diff = (row[''Time''] - prev_row[''Time'']).seconds //3600
if (row[''Area''] == prev_row[''Area'']) & (time_diff == 0):
pass
else:
current_job +=1
df.loc[i+1,''Job no''] = current_job
Con este paso ahora fuera del camino, es una simple cuestión de asignar "Personas" a trabajos individuales:
df= df.sort_values([''Job no'']).reset_index(drop=True)
df[''Person''] = ""
df_groups = df.groupby(''Job no'')
for group in df_groups:
group_size = group[1].count()[''Time'']
for person_idx in range(len(unique_people)):
person = unique_people.loc[person_idx][''Person'']
person_count = df[df[''Person'']==person][''Person''].count()
if group_size <= (3-person_count):
idx = group[1].index.values
df.loc[idx,''Person''] = person
break
Y finalmente,
df= df.sort_values([''Time'']).reset_index(drop=True)
print(df)
He intentado codificar esto de una manera que es más fácil de deseleccionar, por lo que puede haber eficiencias aquí. Sin embargo, el objetivo era establecer la lógica utilizada.
Este código proporciona los resultados esperados en ambos conjuntos de datos, por lo que espero que responda a su pregunta.