Re-assign Column Values In A Pandas Df
Solution 1:
Update
There's a live version of this answer online that you can try for yourself.
Here's an answer in the form of the allocatePeople
function. It's based around precomputing all of the indices where the areas repeat within an hour:
from collections import Counter
import numpy as np
import pandas as pd
defgetAssignedPeople(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 inzip(areas, places, holds):
if (area, place) in assignedPlaces:
# this unique (area, place) has already been assigned to someone
assignedPeople.append(assignedPlaces[(area, place)])
continueif 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 += 1if 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)) andnot 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
defallocatePeople(df, areasPerPerson=3):
assignedPeople = getAssignedPeople(df, areasPerPerson=areasPerPerson)
df = df.copy()
df.loc[:,'Person'] = df['Person'].unique()[assignedPeople]
return df
Note the use of df['Person'].unique()
in allocatePeople
. That handles the case where people are repeated in the input. It is assumed that the order of people in the input is the desired order in which those people should be assigned.
I tested allocatePeople
against the OP's example input (example1
and example2
) and also against a couple of edge cases I came up with that I think(?) match the OP's desired algorithm:
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)
The assert_array_equal
statements pass, and the output matches OP's expected output:
example1TimePlaceAreaOnPerson08:03:00House1A1Person118:17:00House2B2Person128:20:00House3C3Person138:28:00House4D4Person248:35:00House5E5Person3508:40:00House1D6Person2608:42:00House2E7Person3708:45:00House3F8Person2808:50:00House2G9Person3example2TimePlaceAreaOnPerson08:03:00House1X1Person118:17:00House2X2Person128:20:00House3X3Person138:28:00House1X3Person148:35:00House2X3Person158:40:00House3X3Person168:42:00House1X3Person178:45:00House2X3Person188:50:00House3X3Person1
The output for my test cases matches my expectations as well:
long_repeatsTimePlaceAreaPersonOn08:03:00House1APerson1118:17:00House2APerson1228:20:00House3APerson1338:25:00House4APerson2448:30:00House1BPerson2558:31:00House1CPerson3668:35:00House2CPerson3778:45:00House3CPerson3888:50:00House2BPerson29many_repeatsTimePlaceAreaOnPerson08:03:00House1A1Person118:17:00House2B2Person128:20:00House3C3Person138:28:00House4D4Person248:35:00House1D5Person2508:40:00House1E6Person3608:42:00House2E7Person3708:45:00House1F8Person2808:50:00House2F9Person3large_gapTimePlaceAreaOnPerson08:03:00House1A1Person118:17:00House2B2Person128:20:00House3C3Person138:28:00House4D4Person248:35:00House1E5Person3508:40:00House1F6Person3608:42:00House2D7Person2708:45:00House1D8Person2808:50:00House3D9Person3different_timesTimePlaceAreaOnPerson08:03:00House1A1Person118:17:00House2B2Person128:20:00House3C3Person138:28:00House4D4Person248:35:00House1D5Person2508:40:00House1E6Person2609:42:00House2E7Person3709:45:00House1F8Person3809:50:00House1G9Person3
Let me know if it does everything you wanted, or if it still needs some tweaks. I think everyone is eager to see you fulfill your vision.
Solution 2:
Ok, before we delve into the logic of the problem it is worthwhile to do some housekeeping to tidy-up the data and bring it into a more useful format:
#Createtableofunique people
unique_people = df[['Person']].drop_duplicates().sort_values(['Person']).reset_index(drop=True)
#Reformat timecolumn
df['Time'] = pd.to_datetime(df['Time'])
Now, getting to the logic of the problem, it is useful to break the problem down in to stages. Firstly, we will want to create individual jobs (with job numbers) based on the 'Area' and the time between them. i.e. jobs in the same area, within an hour can share the same job number.
#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 inrange(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
With this step now out of the way, it is a simple matter of assigning 'Persons' to individual jobs:
df= df.sort_values(['Job no']).reset_index(drop=True)
df['Person'] = ""
df_groups = df.groupby('Job no')
forgroupin df_groups:
group_size =group[1].count()['Time']
for person_idx inrange(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
And finally,
df= df.sort_values(['Time']).reset_index(drop=True)
print(df)
I've attempted to code this in a way that is easier to unpick, so there may well be efficiencies to be made here. The aim however was to set out the logic used.
This code gives the expected results on both data sets, so I hope it answers your question.
Solution 3:
In writing my other answer, I slowly came around to the idea that the OP's algorithm might be easier to implement with an approach that focuses on the jobs (which can be different), instead of the people (which are all the same). Here's a solution that uses the job-centric approach:
from collections import Counter
import numpy as np
import pandas as pd
defassignJob(job, assignedix, areasPerPerson):
for i inrange(len(assignedix)):
if (areasPerPerson - len(assignedix[i])) >= len(job):
assignedix[i].extend(job)
returnTrueelse:
returnFalsedefallocatePeople(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) inenumerate(zip(areas, holds)):
if hold:
_jobdict[area] = job = _jobdict.get(area, []) + [i]
iflen(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 inrange(npeople)]
for job in jobs:
ifnot assignJob(job, assignedix, areasPerPerson):
# break the job up and try againfor subjob in ([sj] for sj in job):
assignJob(subjob, assignedix, areasPerPerson)
df = df.copy()
for i,aix inenumerate(assignedix):
df.loc[aix, 'Person'] = peopleUniq[i]
return df
This version of allocatePeople
has also been extensively tested and passes all of the same checks described in my other answer.
It does have more looping than my other solution, so it is likely to be slightly less efficient (though it'll only matter if your dataframe is very large, say 1e6
rows and up). On the other hand, it is somewhat shorter and, I think, more straightforward and easy to understand.
Post a Comment for "Re-assign Column Values In A Pandas Df"