División de diccionario / lista dentro de una columna Pandas en columnas separadas

146

Tengo datos guardados en una base de datos postgreSQL. Estoy consultando estos datos usando Python2.7 y convirtiéndolos en un Pandas DataFrame. Sin embargo, la última columna de este marco de datos tiene un diccionario (¿o una lista?) De valores dentro de él. El DataFrame se ve así:

[1] df
Station ID     Pollutants
8809           {"a": "46", "b": "3", "c": "12"}
8810           {"a": "36", "b": "5", "c": "8"}
8811           {"b": "2", "c": "7"}
8812           {"c": "11"}
8813           {"a": "82", "c": "15"}

Necesito dividir esta columna en columnas separadas para que el DataFrame se vea así:

[2] df2
Station ID     a      b       c
8809           46     3       12
8810           36     5       8
8811           NaN    2       7
8812           NaN    NaN     11
8813           82     NaN     15

El principal problema que tengo es que las listas no tienen la misma longitud. Pero todas las listas solo contienen hasta los mismos 3 valores: a, b y c. Y siempre aparecen en el mismo orden (a primero, b segundo, c tercero).

El siguiente código UTILIZADO para funcionar y devolver exactamente lo que quería (df2).

[3] df 
[4] objs = [df, pandas.DataFrame(df['Pollutant Levels'].tolist()).iloc[:, :3]]
[5] df2 = pandas.concat(objs, axis=1).drop('Pollutant Levels', axis=1)
[6] print(df2)

Estaba ejecutando este código la semana pasada y estaba funcionando bien. Pero ahora mi código está roto y recibo este error de la línea [4]:

IndexError: out-of-bounds on slice (end) 

No hice cambios en el código, pero ahora recibo el error. Siento que esto se debe a que mi método no es robusto o adecuado.

Cualquier sugerencia u orientación sobre cómo dividir esta columna de listas en columnas separadas sería muy apreciada.

EDITAR: Creo que los métodos .tolist () y .apply no funcionan en mi código porque es una cadena unicode, es decir:

#My data format 
u{'a': '1', 'b': '2', 'c': '3'}

#and not
{u'a': '1', u'b': '2', u'c': '3'}

Los datos se importan desde la base de datos postgreSQL en este formato. ¿Alguna ayuda o ideas con este problema? ¿Hay alguna manera de convertir el Unicode?

llaffin
fuente
Respondí con una solución ligeramente diferente, pero su código también debería funcionar bien. Usando mi ejemplo ficticio a continuación, esto funciona usando pandas 0.18.1 si omito la ilocparte
joris
¿Es parte de eso que se iloc[:, :3]supone que habrá 3 elementos, y tal vez los segmentos de datos más recientes solo tengan 1 o 2 (por ejemplo, no hay nada bparecido index 8813)?
dwanderson el

Respuestas:

166

Para convertir la cadena a un dict real, puede hacer df['Pollutant Levels'].map(eval). Luego, la solución a continuación se puede usar para convertir el dict a diferentes columnas.


Usando un pequeño ejemplo, puede usar .apply(pd.Series):

In [2]: df = pd.DataFrame({'a':[1,2,3], 'b':[{'c':1}, {'d':3}, {'c':5, 'd':6}]})

In [3]: df
Out[3]:
   a                   b
0  1           {u'c': 1}
1  2           {u'd': 3}
2  3  {u'c': 5, u'd': 6}

In [4]: df['b'].apply(pd.Series)
Out[4]:
     c    d
0  1.0  NaN
1  NaN  3.0
2  5.0  6.0

Para combinarlo con el resto del marco de datos, puede concatlas otras columnas con el resultado anterior:

In [7]: pd.concat([df.drop(['b'], axis=1), df['b'].apply(pd.Series)], axis=1)
Out[7]:
   a    c    d
0  1  1.0  NaN
1  2  NaN  3.0
2  3  5.0  6.0

Usando su código, esto también funciona si omito la ilocparte:

In [15]: pd.concat([df.drop('b', axis=1), pd.DataFrame(df['b'].tolist())], axis=1)
Out[15]:
   a    c    d
0  1  1.0  NaN
1  2  NaN  3.0
2  3  5.0  6.0
joris
fuente
2
Lo he estado usando pd.DataFrame(df[col].tolist())durante mucho tiempo, nunca lo pensé apply(pd.Series). Muy agradable.
ayhan
1
Ahora me doy cuenta del problema. El .apply (pd.Series) no funciona en mi conjunto de datos porque toda la fila es una cadena unicode. Es: u '{' a ':' 1 ',' b ':' 2 ',' c ':' 3 '} y no {u'a': '1', u'b ':' 2 ', u'c ':' 3 '} como muestran sus soluciones. Entonces el código no puede dividirlo en 3 columnas reconocibles.
llaffin
2
@ayhan En realidad, lo probé, ¡y el DataFrame(df['col'].tolist())enfoque es bastante más rápido que el enfoque de aplicación!
joris
3
@llaffin Si es una cadena, puede convertirla en un dict real df[col].map(eval)antes de convertirla en un DataFrame
joris
2
Funciona perfecto, pero es (mucho) más lento que la nueva solución (2019) aportada por Lech Birek stackoverflow.com/a/55355928/2721710
drasc
85

Sé que la pregunta es bastante antigua, pero llegué aquí buscando respuestas. En realidad, ahora hay una manera mejor (y más rápida) de hacer esto usando json_normalize:

import pandas as pd

df2 = pd.json_normalize(df['Pollutant Levels'])

Esto evita las costosas funciones de aplicación ...

Lech Birek
fuente
44
¡Guauu! He estado haciendo funciones de aplicación tediosas y confusas todo el día en Pandas en objetos JSON, y luego me topé con esta respuesta y pensé "¡De ninguna manera, no podría haber sido tan fácil!" Luego lo probé y fue. ¡Muchas gracias!
Emac
El único problema aquí es que no parece copiar sobre otras columnas sin json, lo que significa que si está tratando de normalizar una fila de valores json, tendrá que copiarla y combinar las dos, aún mucho mejor que mi iterativo método. Cudos!
Sr.Drew
para esta solución, ¿cómo sería posible seleccionar dinámicamente la lista de las columnas que deben normalizarse? Los datos transaccionales que estoy trayendo de los .jsonarchivos provienen de diferentes fuentes y no siempre son las mismas columnas que están anidadas. He estado tratando de encontrar una manera de crear una lista de columnas que contengan dictos pero parece que no puede funcionar
Callum Smyth
55
from pandas.io.json import json_normalize
Ramin Melikov
¿Hay alguna manera de aplicar un prefijo a las columnas finales? He notado que hay argumentos como meta_prefixy record_prefix. Sin embargo, no puedo hacer que eso funcione con mi marco de datos (el marco de datos final es correcto en mi caso, pero me gustaría aplicar los prefijos).
J. Snow
21

Pruebe esto: los datos devueltos por SQL deben convertirse en un Dict. o podría ser "Pollutant Levels" ahoraPollutants'

   StationID                   Pollutants
0       8809  {"a":"46","b":"3","c":"12"}
1       8810   {"a":"36","b":"5","c":"8"}
2       8811            {"b":"2","c":"7"}
3       8812                   {"c":"11"}
4       8813          {"a":"82","c":"15"}


df2["Pollutants"] = df2["Pollutants"].apply(lambda x : dict(eval(x)) )
df3 = df2["Pollutants"].apply(pd.Series )

    a    b   c
0   46    3  12
1   36    5   8
2  NaN    2   7
3  NaN  NaN  11
4   82  NaN  15


result = pd.concat([df, df3], axis=1).drop('Pollutants', axis=1)
result

   StationID    a    b   c
0       8809   46    3  12
1       8810   36    5   8
2       8811  NaN    2   7
3       8812  NaN  NaN  11
4       8813   82  NaN  15
Esmerejón
fuente
13

La respuesta de Merlín es mejor y súper fácil, pero no necesitamos una función lambda. La evaluación del diccionario se puede ignorar de forma segura por cualquiera de las dos formas siguientes, como se ilustra a continuación:

Camino 1: dos pasos

# step 1: convert the `Pollutants` column to Pandas dataframe series
df_pol_ps = data_df['Pollutants'].apply(pd.Series)

df_pol_ps:
    a   b   c
0   46  3   12
1   36  5   8
2   NaN 2   7
3   NaN NaN 11
4   82  NaN 15

# step 2: concat columns `a, b, c` and drop/remove the `Pollutants` 
df_final = pd.concat([df, df_pol_ps], axis = 1).drop('Pollutants', axis = 1)

df_final:
    StationID   a   b   c
0   8809    46  3   12
1   8810    36  5   8
2   8811    NaN 2   7
3   8812    NaN NaN 11
4   8813    82  NaN 15

Modo 2: los dos pasos anteriores se pueden combinar de una vez:

df_final = pd.concat([df, df['Pollutants'].apply(pd.Series)], axis = 1).drop('Pollutants', axis = 1)

df_final:
    StationID   a   b   c
0   8809    46  3   12
1   8810    36  5   8
2   8811    NaN 2   7
3   8812    NaN NaN 11
4   8813    82  NaN 15
Hafizur Rahman
fuente
13

Recomiendo encarecidamente que el método extraiga la columna 'Contaminantes':

df_pollutants = pd.DataFrame(df['Pollutants'].values.tolist(), index=df.index)

es mucho más rápido que

df_pollutants = df['Pollutants'].apply(pd.Series)

cuando el tamaño de df es gigante.

usuario9815968
fuente
¡sería genial si pudieras explicar cómo / por qué esto funciona y es mucho mejor! para mí siempre es más rápido y ~ 200 veces más rápido una vez que obtienes más de ~ 1000 filas
Sam Mason
@SamMason cuando hace applytodo el marco de datos es administrado por pandas, pero cuando se trata de valueseso solo juega con el numpy ndarraysque es intrínsecamente más rápido debido al hecho de que tiene cimplementaciones puras .
Sagar Kar
8

Puedes usar joincon pop+ tolist. El rendimiento es comparable a concatcon drop+ tolist, pero algunos pueden encontrar este limpiador de sintaxis:

res = df.join(pd.DataFrame(df.pop('b').tolist()))

Benchmarking con otros métodos:

df = pd.DataFrame({'a':[1,2,3], 'b':[{'c':1}, {'d':3}, {'c':5, 'd':6}]})

def joris1(df):
    return pd.concat([df.drop('b', axis=1), df['b'].apply(pd.Series)], axis=1)

def joris2(df):
    return pd.concat([df.drop('b', axis=1), pd.DataFrame(df['b'].tolist())], axis=1)

def jpp(df):
    return df.join(pd.DataFrame(df.pop('b').tolist()))

df = pd.concat([df]*1000, ignore_index=True)

%timeit joris1(df.copy())  # 1.33 s per loop
%timeit joris2(df.copy())  # 7.42 ms per loop
%timeit jpp(df.copy())     # 7.68 ms per loop
jpp
fuente
3

Una solución de línea es la siguiente:

>>> df = pd.concat([df['Station ID'], df['Pollutants'].apply(pd.Series)], axis=1)
>>> print(df)
   Station ID    a    b   c
0        8809   46    3  12
1        8810   36    5   8
2        8811  NaN    2   7
3        8812  NaN  NaN  11
4        8813   82  NaN  15
Jaroslav Bezděk
fuente
1

my_df = pd.DataFrame.from_dict(my_dict, orient='index', columns=['my_col'])

.. habría analizado el dict correctamente (colocando cada clave dict en una columna df separada, y los valores clave en filas df), para que los dictos no se aplasten en una sola columna en primer lugar.

mirekphd
fuente
0

He concatenado esos pasos en un método, debe pasar solo el marco de datos y la columna que contiene el dict para expandir:

def expand_dataframe(dw: pd.DataFrame, column_to_expand: str) -> pd.DataFrame:
    """
    dw: DataFrame with some column which contain a dict to expand
        in columns
    column_to_expand: String with column name of dw
    """
    import pandas as pd

    def convert_to_dict(sequence: str) -> Dict:
        import json
        s = sequence
        json_acceptable_string = s.replace("'", "\"")
        d = json.loads(json_acceptable_string)
        return d    

    expanded_dataframe = pd.concat([dw.drop([column_to_expand], axis=1),
                                    dw[column_to_expand]
                                    .apply(convert_to_dict)
                                    .apply(pd.Series)],
                                    axis=1)
    return expanded_dataframe
Emanuel Fontelles
fuente
-1
df = pd.concat([df['a'], df.b.apply(pd.Series)], axis=1)
Siraj S.
fuente