Условное слияние строк фрейма данных

#python #pandas

#python #панды

Вопрос:

У меня есть фрейм данных 2xN сообщений чата, и я пытаюсь найти самый чистый способ объединения последовательных сообщений, исходящих от одного и того же говорящего. Вот пример данных, с которыми я работаю:

 mydata = pd.DataFrame(data=[['A','random text'],
                            ['B','random text'],
                            ['A','random text'],
                            ['A','random text'],
                            ['A','random text'],
                            ['B','random text'],
                            ['A','random text'],
                            ['B','random text'],
                            ['B','random text'],
                            ['A','random text']], columns=['speaker','message'])
 

Надеюсь, вы видите, что порядок динамиков не в формате ABAB, как хотелось бы. Вместо этого есть несколько последовательностей AAAB и ABBA. В настоящее время я думаю перестроить фрейм данных с нуля, сверяя идентификатор каждой строки с идентификатором следующей позиции индекса…

 mergeCheck = True
while mergeCheck is True:
    # set length of the dataframe
    lenDF = len(mydata)
# empty list to rebuild dataframe
mergeDF = []
# set index position at the beginning of dataframe
i = 0            
while i < lenDF-1:
   # check whether adjacent rows have different ID
   if mydata['speaker'].iloc[i] != mydata['speaker'].iloc[i 1]:
       # if true, append row as is to mergeDF list
       mergeDF.append([mydata['speaker'].iloc[i],
                       mydata['message'].iloc[i]])
       # increase index position by 1
       i  =1
   else:
       # merge messages
       mergeDF.append([mydata['speaker'].iloc[i],
                       mydata['message'].iloc[i]   mydata['message'].iloc[i 1]])
       # increase index position by 2
       i  =2
# exit the loop if index position falls on the last message
if i == lenDF-1: 
    # if true, append row as is to mergeDF list
    mergeDF.append([mydata['speaker'].iloc[i],
                    mydata['message'].iloc[i]])
    # increase counter by 1
    i  =1
if i == lenDF:
    mergeCheck = False
 

Однако это работает только для двух смежных сообщений. Возвращаясь к моим исходным данным, при вводе в фрейм данных приведенная выше функция генерирует следующий вывод…

 --------------------------
  speaker  |   message
--------------------------
    A         'random text'
    B         'random text'
    A         'random textrandom text'
    A         'random text'
    B         'random text'
    A         'random text'
    B         'random textrandom text'
    A         'random text'
--------------------------
 

Я подумал расширить функцию, чтобы проверить больше сравнений i (т. Е. Выполняет ‘.iloc [i] != .iloc [i 2]’ или ‘.iloc [i] ! = .iloc [i 3]’ и т.д.), Но это становится неработоспособным очень быстро. Я думаю, мне нужен какой-то способ повторить описанную выше функцию, пока фрейм данных не будет в желаемом формате. Но я не уверен, как это сделать.

Ответ №1:

Возможное решение заключается в следующем:

 df1 = mydata[mydata['speaker']=='A'].reset_index()
df2= mydata[mydata['speaker']=='B'].reset_index()
df = pd.concat([df1, df2]).sort_index()
 

который возвращает

   index speaker      message
0      0       A  random text
0      1       B  random text
1      2       A  random text
1      5       B  random text
2      3       A  random text
2      7       B  random text
3      4       A  random text
3      8       B  random text
4      6       A  random text
5      9       A  random tex
 

если у вас есть временная метка для них, не забудьте отсортировать по времени / дате перед сбросом индекса. Кроме того, при объединении остерегайтесь времени.

Редактировать

После ваших разъяснений в комментариях я предлагаю это. Сначала создайте ключ, который соответствует равным объектам (A, B), а затем сгруппируйте по динамикам и объектам (ключам)

 df['key'] = (df['speaker'] != df['speaker'].shift(1)).astype(int).cumsum()
 

что дает

   speaker      message  key
0       A  random text    1
1       B  random text    2
2       A  random text    3
3       A  random text    3
4       A  random text    3
5       B  random text    4
6       A  random text    5
7       B  random text    6
8       B  random text    6
9       A  random text    7
 

Теперь вам просто нужно groupby

 df = df.groupby(['key', 'speaker'])['message'].apply(' '.join)
df
 

что дает

 key  speaker
1    A                                  random text
2    B                                  random text
3    A          random text random text random text
4    B                                  random text
5    A                                  random text
6    B                      random text random text
7    A                                  random text
 

Комментарии:

1. Спасибо за ввод — К сожалению, это не объединяет столбцы сообщений вместе. Я, вероятно, должен был прояснить это в исходном сообщении!

2. Ах! Теперь это имеет смысл. Я отредактировал свой ответ.

3. Спасибо, это здорово и намного короче, чем мой подход!

Ответ №2:

После некоторого изучения я нашел лучшее решение, чем мой OP. Я подробно расскажу об этом здесь для всех, кто сталкивается с подобной проблемой. Я пока воздержусь от принятия своего собственного ответа на случай, если кто-то предложит лучший вариант.

 # compare each row with the previous
mydata['prev_speaker'] = mydata['speaker'].shift(1).mask(pd.isnull, mydata['speaker'])

# boolean value to determine whether current speaker differs from previous
mydata['speaker_change'] = np.where(mydata['speaker'] != mydata['prev_speaker'], 'True','False')

# empty list to record changes in speaker
counterList = []    

# initialize a counter to loop through dataframe
counter =1

# loop through dataframe, increasing counter by 1 if the speaker changes
for row in mydata['speaker_change']:
    if row == 'False':
        counterList.append(counter)
    else:
        counter =1
        counterList.append(counter)

# add counterList to dataframe
mydata['chunking'] = counterList

# group the original message based on the chunking variable
mydata['message'] = mydata.groupby(['chunking'])['message'].transform(lambda x: ' '.join(x))

# drop duplicate rows based on message content and chunking
mydata = mydata.drop_duplicates(subset=['message','chunking'])

# drop non-needed columns
mydata = mydata.drop(['prev_speaker','speaker_change','chunking'], axis=1)
 

Что теперь дает мне следующее:

 |---------------------|-------------------------------------|
|       Speaker       |               Message               |
|---------------------|-------------------------------------|
|          A          |             random text             |
|---------------------|-------------------------------------|
|          B          |             random text             |
|---------------------|-------------------------------------|
|          A          | random text random text random text |
|---------------------|-------------------------------------|
|          B          |             random text             |
|---------------------|-------------------------------------|
|          A          |             random text             |
|---------------------|-------------------------------------|
|          B          |       random text random text       |
|---------------------|-------------------------------------|
|          A          |             random text             |
|---------------------|-------------------------------------|