J'ai un script
qui attribue une valeur basée sur deux columns
dans un pandas
df
. Le code ci-dessous est capable d'implémenter la 1ère étape mais j'ai du mal avec la seconde.
Donc, le script devrait initialement:
1) Attribuez une Person
à chaque personne string
dans [Area]
et le premier 3 unique values
dans [Place]
2) Cherchez à réaffecter People
avec moins de 3 unique values
Exemple. La df
ci-dessous a 6 unique values
dans [Area]
et [Place]
. Mais 3 People
sont attribués. Dans l’idéal, 2
personnes 2 unique values
chacune
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 ')
Sortie:
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
Comme vous pouvez le constater, la première étape fonctionne bien. ou chaque personne string
dans [Area]
, le premier 3 unique values
dans [Place]
sont affectés à une Person
. Cela laisse Person 1
avec 3 values
, Person 2
avec 1 value
et Person 3
avec 2 values
.
La deuxième étape est où je me bats.
Si Person
a moins de 3 unique values
qui leur est attribué, modifiez-le afin que chaque Person
ait jusqu'à 3 unique values
Sortie prévue:
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
La description:
Person 1
avait déjà 3 unique values
assigné à tout bon. Person 2
et 3
en avaient moins, nous devrions donc essayer de les combiner. Toutes les valeurs en double doivent rester les mêmes.
Autant que je sache, vous êtes satisfait de tout ce qui précède la répartition des personnes. Voici donc une solution prête à l'emploi qui permet de "fusionner" des personnes avec moins de 3 valeurs uniques, de sorte que chaque personne se voit attribuer 3 valeurs uniques, à l'exception de la dernière évidemment (sur la base de l'avant-dernier df que vous avez publié ("Sortie:") sans toucher ceux qui ont déjà 3 valeurs uniques et ne fait que fusionner les autres.
EDIT: Code très simplifié. Encore une fois, en prenant votre DF comme entrée:
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)
Dans ce qui suit, j'ai ajouté quelques lignes avant les dernières lignes de votre code:
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 ')
Pour être honnête, je ne suis pas sûr que cela fonctionne dans tous les cas, mais cela donne le résultat escompté dans le cas de test.
Voyons si je peux aider avec une compréhension limitée de ce que vous essayez de faire.
Vous avez sequential data (je les appellerai des événements) et vous souhaitez attribuer à chaque événement un identifiant "personne". L'identifiant que vous attribuez à chaque événement dépend des attributions précédentes et il me semble qu'il doit être régi par les règles suivantes pour être appliqué de manière séquentielle:
Je te connais : Je peux réutiliser un identifiant précédent si: les mêmes valeurs pour "Place" et "Area" sont déjà apparues pour un identifiant donné (a quelque chose à voir avec le temps?).
Je ne vous connais pas : Je vais créer un nouvel identifiant si: une nouvelle valeur de Area apparaît (donc Place et Area jouent des rôles différents?).
je vous connais? : Je pourrais réutiliser un identifiant utilisé précédemment si: un identifiant n'a pas été attribué à au moins trois événements (et si cela se produit pour plusieurs identifiants? Je suppose que j'utilise le plus ancien ...).
nah, je ne sais pas : si aucune des règles précédentes ne s'applique, je vais créer un nouvel identifiant.
Après avoir supposé ce qui précède, voici une implémentation d’une solution:
# 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
Sortie:
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
et le dataframe final est, comme vous voulez:
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
Remarque : Notez que j'ai volontairement évité d'utiliser groupé par opérations et les données traitées de manière séquentielle. Je pense que ce genre de complexité (et ne comprenant pas vraiment ce que vous voulez faire ...) appelle cette approche. En outre, vous pouvez adapter les règles pour qu'elles soient plus compliquées (le temps joue-t-il vraiment un rôle ou pas?) En utilisant la même structure que ci-dessus.
En regardant de nouvelles données, il est évident que je ne comprenais pas ce que vous tentiez de faire (en particulier, la cession ne semble pas suivre des règles séquentielles ) . J'aurais une solution qui fonctionnerait avec votre deuxième jeu de données, mais cela donnerait un résultat différent pour le premier jeu de données.
La solution est beaucoup plus simple et ajoutera une colonne (que vous pourrez supprimer plus tard si vous le souhaitez):
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)
Sur le deuxième jeu de données, cela donnerait:
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
Sur le premier jeu de données, cela donne:
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
Cela diffère de votre sortie prévue aux index 2 et 3. Cette sortie convient-elle à vos besoins? Pourquoi pas?
Premièrement, cette réponse n’est pas liée à votre exigence de seulement réaffecter restes (donc je ne m’attends pas à ce que vous l’acceptiez). Cela dit, je le publie quand même parce que votre contrainte de fenêtre temporelle était difficile à résoudre dans un monde de pandas. Peut-être que ma solution ne vous sera pas utile pour le moment, mais peut-être plus tard;) À tout le moins, ce fut une expérience d'apprentissage pour moi - alors peut-être que d'autres pourront en tirer profit.
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' ] ] )
Et l'exemple de sortie sur ma machine ressemble à ceci:
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)
>>>
Remarques:
lookback
contrôle le temps passé à regarder en arrière pour considérer les emplacements attribués à une personneLookback
indique l'heure de coupureevaluateLookback
est appelé à plusieurs reprises pour chaque ligne de la table, df
étant l'ensemble du DataFrame, idx
l'index/libellé actuel et dfg
la ligne actuelle. lambdaProxy
contrôle l'appel de evaluateLookback
.3
mais peut être ajusté si nécessairelambdaProxy
, puis ce résultat stocké et utilisé dans evaluateLookback
Il y a quelques cas intéressants dans la sortie de démonstration: 10:39:00
, 14:48:00
, 17:09:00
De plus: il serait intéressant de voir la "colonne des fonctions" dans les pandas, avec peut-être une capacité semblable à la mémorisation? Idéalement, la colonne 'Personne' devrait comporter une fonction et un calcul sur demande, soit avec sa propre ligne, soit avec une vue de fenêtre variable. Quelqu'un a vu quelque chose comme ça?
Que diriez-vous de cela pour l'étape 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
)
Le résultat:
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