Regrouper les valeurs des colonnes dans un DF pandas
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 première étape, mais j'ai du mal avec la seconde.
ainsi le script devrait d'abord:
1) Assigner un Person
pour chaque individu string
[Area]
et le premier 3 unique values
[Place]
2) Chercher à réassigner People
avec moins de!--10-->
Exemple. df
ci-dessous ont 6 unique values
[Area]
et [Place]
. Mais 3 People
sont affectés. Idéalement, 2
gens 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 ')
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 voir, la première étape fonctionne très bien. ou chaque individu string
[Area]
le premier 3 unique values
[Place]
sont assignés à un Person
. Cela laisse Person 1
3 values
,Person 2
1 value
et Person 3
2 values
.
la deuxième étape est là où je lutte.
si a Person
a moins que 3 unique values
affecté à eux, de modifier le présent de sorte que chaque Person
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
Description:
Person 1
déjà eu 3 unique values
attribué à toutes les bonnes. Person 2
et 3
a moins que nous devrions chercher à les combiner. Toutes les valeurs dupliquées doivent rester les mêmes.
4 réponses
d'après ce que j'ai compris, vous êtes heureux avec tout avant l'affectation de la personne. Donc voici une solution plug and play pour "fusionner"les personnes avec moins de 3 valeurs uniques donc chaque personne se retrouve avec 3 valeurs uniques à l'exception de la dernière évidemment (basé sur la seconde à la dernière df que vous avez posté ("sortie:") sans toucher ceux qui ont déjà 3 valeurs uniques et fusionne juste les autres.
EDIT: code grandement simplifié. Encore une fois, prendre 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)
essai en cours
Dans la suite, j'ai ajouté quelques lignes avant les dernières lignes de ton 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 votre résultat prévu dans le cas de test.
les tentatives Précédentes
voyons voir si je suis capable d'aider avec une compréhension limitée de ce que vous essayez de faire.
Vous avez séquentiel données (je vais les appeler événements) et vous voulez attribuer à chaque événement un identificateur "personne". L'identificateur que vous assignerez à chaque événement successif dépend des assignations antérieures et il me semble qu'il doit être régi par les règles suivantes à appliquer de façon séquentielle:
je sais que vous: je peux réutiliser un précédent identifiant si: mêmes valeurs pour le "Lieu" et "Zone", déjà paru pour un identifiant donné (a le temps quelque chose à faire avec il?).
je ne sais PAS vous: je vais créer un nouvel identifiant, si: une nouvelle valeur de la Zone s'affiche ( alors L'endroit et la zone jouent des rôles différents?).
est-ce que je vous connais?: je pourrais réutiliser précédemment utilisées identificateur du si: un identifiant n'a pas été attribué à au moins trois événements (que faire si cela se produit pour plusieurs identifiants? je suppose que je utiliser le aîné...).
nah, je ne suis pas d': si aucune des règles précédentes ne s'applique, je vais créer un nouvel identifiant.
ayant supposé ce qui précède, ce qui suit est 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 la dernière dataframe 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 regroupés par les opérations et les traités données séquentielles. Je pense que ce genre de complexité (et pas vraiment comprendre ce que vous voulez faire...) appelle à cette approche. Aussi, vous pouvez adapter les règles pour être plus compliqué (est temps de vraiment jouer un rôle ou pas?) utilisant la même structure ci-dessus.
réponse mise à jour pour les nouvelles données
regarder de nouvelles données il est évident que je n'ai pas compris ce que vous essayez de faire (en particulier, la cession ne semble pas suivre séquentielle règles). J'aurais une solution qui fonctionnerait sur votre deuxième ensemble de données, mais cela donnerait un résultat différent pour le premier ensemble de données.
La solution est beaucoup plus simple et va ajouter une colonne (que vous pouvez vous déplacer 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 second ensemble de données, il 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, il 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
ceci est différent de votre sortie prévue sur les index 2 et 3. Est-ce la sortie bien avec vos besoins? Pourquoi pas?
tout D'abord, cette réponse ne correspond pas à votre exigence de réaffecter les restes (donc je ne m'attends pas à ce que vous les acceptiez). Cela dit, je le poste de toute façon parce que votre contrainte de fenêtre temporelle était délicate à résoudre dans un monde de pandas. Peut - être que ma solution ne sera pas utile pour vous en ce moment, mais peut-être plus tard ;) à tout le moins, ce fut une expérience d'apprentissage pour moi-donc peut-être que d'autres peuvent 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 la sortie d'échantillon 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)
>>>
Notes:
lookback
variable contrôle le temps passé à regarder en arrière pour considérer les emplacements comme attribués à une personneLookback
la colonne montre l'Heure de coupureevaluateLookback
est appelée à plusieurs reprises pour chaque ligne de la table, avecdf
en cours de l'ensemble du DataFrame,idx
l'index en cours ou de l'étiquette, etdfg
la ligne actuelle.lambdaProxy
contrôle l'appel deevaluateLookback
.- le nombre d'emplacements par personne est fixé à
3
mais cela pourrait être ajusté au besoin - des exigences arbitrairement Complexes pour la période de retour peuvent être gérées en ayant une autre colonne func qui est d'abord évaluée par
lambdaProxy
et puis ce résultat stocké et utilisé dansevaluateLookback
Il y a quelques intéressants bord de cas dans la démo de sortie: 10:39:00
,14:48:00
,17:09:00
mis à part: il serait être intéressant de voir "fonctions colonne" dans pandas, peut-être avec Mémoriser-comme la capacité? Idéalement, la colonne "personne" devrait prendre une fonction et calquer sur demande, soit avec sa propre rangée, soit avec une vue de fenêtre variable. Quelqu'un voit quelque chose comme ça?
Que diriez-vous de ceci 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