python - what - Dividir(explodir) a entrada da cadeia de dados do pandas para separar linhas




what is pandas python (11)

Eu tenho um pandas dataframe em que uma coluna de seqüências de caracteres de texto contém valores separados por vírgulas. Desejo dividir cada campo CSV e criar uma nova linha por entrada (suponha que CSV esteja limpo e precise ser dividido em ','). Por exemplo, a deve se tornar b :

In [7]: a
Out[7]: 
    var1  var2
0  a,b,c     1
1  d,e,f     2

In [8]: b
Out[8]: 
  var1  var2
0    a     1
1    b     1
2    c     1
3    d     2
4    e     2
5    f     2

Até agora, tentei várias funções simples, mas o método .apply parece aceitar apenas uma linha como valor de retorno quando é usada em um eixo, e não consigo .transform com que .transform funcione. Qualquer sugestão será muito bem-vinda!

Exemplo de dados:

from pandas import DataFrame
import numpy as np
a = DataFrame([{'var1': 'a,b,c', 'var2': 1},
               {'var1': 'd,e,f', 'var2': 2}])
b = DataFrame([{'var1': 'a', 'var2': 1},
               {'var1': 'b', 'var2': 1},
               {'var1': 'c', 'var2': 1},
               {'var1': 'd', 'var2': 2},
               {'var1': 'e', 'var2': 2},
               {'var1': 'f', 'var2': 2}])

Sei que isso não funcionará porque perdemos metadados do DataFrame passando por numpy, mas isso deve lhe dar uma ideia do que tentei fazer:

def fun(row):
    letters = row['var1']
    letters = letters.split(',')
    out = np.array([row] * len(letters))
    out['var1'] = letters
a['idx'] = range(a.shape[0])
z = a.groupby('idx')
z.transform(fun)

TL; DR

import pandas as pd
import numpy as np

def explode_str(df, col, sep):
    s = df[col]
    i = np.arange(len(s)).repeat(s.str.count(sep) + 1)
    return df.iloc[i].assign(**{col: sep.join(s).split(sep)})

def explode_list(df, col):
    s = df[col]
    i = np.arange(len(s)).repeat(s.str.len())
    return df.iloc[i].assign(**{col: np.concatenate(s)})

Demonstração

explode_str(a, 'var1', ',')

  var1  var2
0    a     1
0    b     1
0    c     1
1    d     2
1    e     2
1    f     2

Vamos criar um novo dataframe d que tenha listas

d = a.assign(var1=lambda d: d.var1.str.split(','))

explode_list(d, 'var1')

  var1  var2
0    a     1
0    b     1
0    c     1
1    d     2
1    e     2
1    f     2

Comentários gerais

np.arange com repeat para produzir posições do índice de dataframe que eu possa usar com o iloc .

Perguntas frequentes

Por que eu não uso o loc ?

Como o índice pode não ser exclusivo e o uso de loc retornará todas as linhas correspondentes a um índice consultado.

Por que você não usa o atributo de values e fatia isso?

Ao chamar values , se a totalidade do dataframe estiver em um "bloco" coeso, o Pandas retornará uma visão da matriz que é o "bloco". Caso contrário, os Pandas terão que montar um novo array. Quando cobbled, essa matriz deve ser de um tipo uniforme. Muitas vezes isso significa retornar um array com dtype que é object . Usando iloc vez de fatiar o atributo values , eu me alivio de ter que lidar com isso.

Por que você usa assign ?

Quando uso a assign usando o mesmo nome de coluna que estou explodindo, sobrescrevo a coluna existente e mantenho sua posição no dataframe.

Por que os valores do índice são repetidos?

Em virtude de usar o iloc em posições repetidas, o índice resultante mostra o mesmo padrão repetido. Uma repetição para cada elemento da lista ou string.
Isso pode ser redefinido com reset_index(drop=True)

Para cordas

Eu não quero ter que dividir as cordas prematuramente. Então, em vez disso, eu conto as ocorrências do argumento sep assumindo que, se eu fosse dividir, o comprimento da lista resultante seria um a mais que o número de separadores.

Eu então uso esse sep para join as cordas e depois split .

def explode_str(df, col, sep):
    s = df[col]
    i = np.arange(len(s)).repeat(s.str.count(sep) + 1)
    return df.iloc[i].assign(**{col: sep.join(s).split(sep)})

Para listas

Semelhante às strings, exceto que não preciso contar as ocorrências do sep porque sep já estão divididas.

Eu uso o concatenate de Numpy para juntar as listas.

import pandas as pd
import numpy as np

def explode_list(df, col):
    s = df[col]
    i = np.arange(len(s)).repeat(s.str.len())
    return df.iloc[i].assign(**{col: np.concatenate(s)})

A divisão da função string pode pegar um argumento booleano de opção 'expandir'.

Aqui está uma solução usando este argumento:

a.var1.str.split(",",expand=True).set_index(a.var2).stack().reset_index(level=1, drop=True).reset_index().rename(columns={0:"var1"})

Aqui está uma função que escrevi para essa tarefa comum. É mais eficiente que os métodos Series / stack . A ordem e os nomes das colunas são mantidos.

def tidy_split(df, column, sep='|', keep=False):
    """
    Split the values of a column and expand so the new DataFrame has one split
    value per row. Filters rows where the column is missing.

    Params
    ------
    df : pandas.DataFrame
        dataframe with the column to split and expand
    column : str
        the column to split and expand
    sep : str
        the string used to split the column's values
    keep : bool
        whether to retain the presplit value as it's own row

    Returns
    -------
    pandas.DataFrame
        Returns a dataframe with the same columns as `df`.
    """
    indexes = list()
    new_values = list()
    df = df.dropna(subset=[column])
    for i, presplit in enumerate(df[column].astype(str)):
        values = presplit.split(sep)
        if keep and len(values) > 1:
            indexes.append(i)
            new_values.append(presplit)
        for value in values:
            indexes.append(i)
            new_values.append(value)
    new_df = df.iloc[indexes, :].copy()
    new_df[column] = new_values
    return new_df

Com essa função, a pergunta original é tão simples quanto:

tidy_split(a, 'var1', sep=',')

Aqui está uma mensagem bastante direta que usa o método split do acessor pandas str e, em seguida, usa o NumPy para nivelar cada linha em uma única matriz.

Os valores correspondentes são recuperados, repetindo-se a coluna não dividida, o número correto de vezes com np.repeat .

var1 = df.var1.str.split(',', expand=True).values.ravel()
var2 = np.repeat(df.var2.values, len(var1) / len(df))

pd.DataFrame({'var1': var1,
              'var2': var2})

  var1  var2
0    a     1
1    b     1
2    c     1
3    d     2
4    e     2
5    f     2

Depois de uma dolorosa experimentação para encontrar algo mais rápido do que a resposta aceita, consegui que isso funcionasse. Ele foi executado 100 vezes mais rápido no conjunto de dados em que o testei.

Se alguém souber uma maneira de tornar isso mais elegante, por favor modifique meu código. Não consegui encontrar uma maneira que funcionasse sem definir as outras colunas que você deseja manter como o índice e, em seguida, redefinir o índice e renomear as colunas, mas imagino que exista outra coisa que funcione.

b = DataFrame(a.var1.str.split(',').tolist(), index=a.var2).stack()
b = b.reset_index()[[0, 'var2']] # var1 variable is currently labeled 0
b.columns = ['var1', 'var2'] # renaming var1

Eu criei uma solução para quadros de dados com números arbitrários de colunas (enquanto ainda separava as entradas de uma coluna por vez).

def splitDataFrameList(df,target_column,separator):
    ''' df = dataframe to split,
    target_column = the column containing the values to split
    separator = the symbol used to perform the split

    returns: a dataframe with each entry for the target column separated, with each element moved into a new row. 
    The values in the other columns are duplicated across the newly divided rows.
    '''
    def splitListToRows(row,row_accumulator,target_column,separator):
        split_row = row[target_column].split(separator)
        for s in split_row:
            new_row = row.to_dict()
            new_row[target_column] = s
            row_accumulator.append(new_row)
    new_rows = []
    df.apply(splitListToRows,axis=1,args = (new_rows,target_column,separator))
    new_df = pandas.DataFrame(new_rows)
    return new_df

Eu tenho lutado com a experiência de falta de memória usando várias maneiras de explodir minhas listas, então preparei alguns benchmarks para me ajudar a decidir quais as respostas para upvote. Eu testei cinco cenários com proporções variáveis ​​do comprimento da lista para o número de listas. Compartilhando os resultados abaixo:

Tempo: (menos é melhor, clique para ver versão grande)

Uso de memória de pico: (menos é melhor)

Conclusões

  • @ Resposta MaxU (atualização 2), codinome concatenar oferece a melhor velocidade em quase todos os casos, mantendo o baixo consumo de memória peek,
  • veja a resposta do @Digulligan ( pilha de nome de código) se você precisar processar muitas linhas com listas relativamente pequenas e puder aumentar a memória de pico,
  • A resposta do @ Chang aceito funciona bem para quadros de dados que têm poucas linhas, mas listas muito grandes.

Detalhes completos (funções e código de benchmarking) estão neste gitHub gist . Observe que o problema do benchmark foi simplificado e não incluiu a divisão de strings na lista - que a maioria das soluções executou de maneira semelhante.


Existe a possibilidade de dividir e explodir o dataframe sem alterar a estrutura do dataframe

Entrada:

    var1    var2
0   a,b,c   1
1   d,e,f   2



#Get the indexes which are repetative with the split 
df = df.reindex(df.index.repeat(df.var1.str.split(',').apply(len)))
#Assign the split values to dataframe column  
df['var1'] = np.hstack(df['var1'].drop_duplicates().str.split(','))

Fora:

    var1    var2
0   a   1
0   b   1
0   c   1
1   d   2
1   e   2
1   f   2

Pergunta semelhante como: pandas: Como divido o texto em uma coluna em várias linhas?

Você poderia fazer:

>> a=pd.DataFrame({"var1":"a,b,c d,e,f".split(),"var2":[1,2]})
>> s = a.var1.str.split(",").apply(pd.Series, 1).stack()
>> s.index = s.index.droplevel(-1)
>> del a['var1']
>> a.join(s)
   var2 var1
0     1    a
0     1    b
0     1    c
1     2    d
1     2    e
1     2    f

Que tal algo como isso:

In [55]: pd.concat([Series(row['var2'], row['var1'].split(','))              
                    for _, row in a.iterrows()]).reset_index()
Out[55]: 
  index  0
0     a  1
1     b  1
2     c  1
3     d  2
4     e  2
5     f  2

Então você só precisa renomear as colunas


UPDATE2: função vectorizada mais genérica, que funcionará para várias colunas de list normal e múltipla

def explode(df, lst_cols, fill_value='', preserve_index=False):
    # make sure `lst_cols` is list-alike
    if (lst_cols is not None
        and len(lst_cols) > 0
        and not isinstance(lst_cols, (list, tuple, np.ndarray, pd.Series))):
        lst_cols = [lst_cols]
    # all columns except `lst_cols`
    idx_cols = df.columns.difference(lst_cols)
    # calculate lengths of lists
    lens = df[lst_cols[0]].str.len()
    # preserve original index values    
    idx = np.repeat(df.index.values, lens)
    # create "exploded" DF
    res = (pd.DataFrame({
                col:np.repeat(df[col].values, lens)
                for col in idx_cols},
                index=idx)
             .assign(**{col:np.concatenate(df.loc[lens>0, col].values)
                            for col in lst_cols}))
    # append those rows that have empty lists
    if (lens == 0).any():
        # at least one list in cells is empty
        res = (res.append(df.loc[lens==0, idx_cols], sort=False)
                  .fillna(fill_value))
    # revert the original index order
    res = res.sort_index()
    # reset index if requested
    if not preserve_index:        
        res = res.reset_index(drop=True)
    return res

Demonstração:

Várias colunas da list - todas as colunas da list devem ter o mesmo número de elementos em cada linha:

In [134]: df
Out[134]:
   aaa  myid        num          text
0   10     1  [1, 2, 3]  [aa, bb, cc]
1   11     2         []            []
2   12     3     [1, 2]      [cc, dd]
3   13     4         []            []

In [135]: explode(df, ['num','text'], fill_value='')
Out[135]:
   aaa  myid num text
0   10     1   1   aa
1   10     1   2   bb
2   10     1   3   cc
3   11     2
4   12     3   1   cc
5   12     3   2   dd
6   13     4

preservando os valores originais do índice:

In [136]: explode(df, ['num','text'], fill_value='', preserve_index=True)
Out[136]:
   aaa  myid num text
0   10     1   1   aa
0   10     1   2   bb
0   10     1   3   cc
1   11     2
2   12     3   1   cc
2   12     3   2   dd
3   13     4

Configuração:

df = pd.DataFrame({
 'aaa': {0: 10, 1: 11, 2: 12, 3: 13},
 'myid': {0: 1, 1: 2, 2: 3, 3: 4},
 'num': {0: [1, 2, 3], 1: [], 2: [1, 2], 3: []},
 'text': {0: ['aa', 'bb', 'cc'], 1: [], 2: ['cc', 'dd'], 3: []}
})

Coluna CSV:

In [46]: df
Out[46]:
        var1  var2 var3
0      a,b,c     1   XX
1  d,e,f,x,y     2   ZZ

In [47]: explode(df.assign(var1=df.var1.str.split(',')), 'var1')
Out[47]:
  var1  var2 var3
0    a     1   XX
1    b     1   XX
2    c     1   XX
3    d     2   ZZ
4    e     2   ZZ
5    f     2   ZZ
6    x     2   ZZ
7    y     2   ZZ

Usando este pequeno truque podemos converter coluna semelhante a CSV para a coluna de list :

In [48]: df.assign(var1=df.var1.str.split(','))
Out[48]:
              var1  var2 var3
0        [a, b, c]     1   XX
1  [d, e, f, x, y]     2   ZZ

ATUALIZAÇÃO: abordagem vetorializada genérica (funcionará também para várias colunas):

DF original:

In [177]: df
Out[177]:
        var1  var2 var3
0      a,b,c     1   XX
1  d,e,f,x,y     2   ZZ

Solução:

Primeiro vamos converter strings CSV para listas:

In [178]: lst_col = 'var1' 

In [179]: x = df.assign(**{lst_col:df[lst_col].str.split(',')})

In [180]: x
Out[180]:
              var1  var2 var3
0        [a, b, c]     1   XX
1  [d, e, f, x, y]     2   ZZ

Agora podemos fazer isso:

In [181]: pd.DataFrame({
     ...:     col:np.repeat(x[col].values, x[lst_col].str.len())
     ...:     for col in x.columns.difference([lst_col])
     ...: }).assign(**{lst_col:np.concatenate(x[lst_col].values)})[x.columns.tolist()]
     ...:
Out[181]:
  var1  var2 var3
0    a     1   XX
1    b     1   XX
2    c     1   XX
3    d     2   ZZ
4    e     2   ZZ
5    f     2   ZZ
6    x     2   ZZ
7    y     2   ZZ

Resposta antiga:

Inspirado pela solução @AFinkelstein , eu queria torná-lo um pouco mais generalizado, o qual poderia ser aplicado ao DF com mais de duas colunas e tão rápido, bem quase tão rápido quanto a solução de AFinkelstein):

In [2]: df = pd.DataFrame(
   ...:    [{'var1': 'a,b,c', 'var2': 1, 'var3': 'XX'},
   ...:     {'var1': 'd,e,f,x,y', 'var2': 2, 'var3': 'ZZ'}]
   ...: )

In [3]: df
Out[3]:
        var1  var2 var3
0      a,b,c     1   XX
1  d,e,f,x,y     2   ZZ

In [4]: (df.set_index(df.columns.drop('var1',1).tolist())
   ...:    .var1.str.split(',', expand=True)
   ...:    .stack()
   ...:    .reset_index()
   ...:    .rename(columns={0:'var1'})
   ...:    .loc[:, df.columns]
   ...: )
Out[4]:
  var1  var2 var3
0    a     1   XX
1    b     1   XX
2    c     1   XX
3    d     2   ZZ
4    e     2   ZZ
5    f     2   ZZ
6    x     2   ZZ
7    y     2   ZZ




dataframe