Diviser le dictionnaire / la liste à l'intérieur d'une colonne Pandas en colonnes séparées

148

J'ai des données enregistrées dans une base de données postgreSQL. J'interroge ces données en utilisant Python2.7 et je les transforme en un Pandas DataFrame. Cependant, la dernière colonne de ce dataframe contient un dictionnaire (ou une liste?) De valeurs. Le DataFrame ressemble à ceci:

[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"}

Je dois diviser cette colonne en colonnes séparées pour que le DataFrame ressemble à ceci:

[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

Le principal problème que j'ai, c'est que les listes ne sont pas de la même longueur. Mais toutes les listes ne contiennent que les 3 mêmes valeurs: a, b et c. Et ils apparaissent toujours dans le même ordre (un premier, b deuxième, c troisième).

Le code suivant utilisé pour fonctionner et retourner exactement ce que je voulais (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)

J'exécutais ce code la semaine dernière et cela fonctionnait bien. Mais maintenant, mon code est cassé et j'obtiens cette erreur de la ligne [4]:

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

Je n'ai apporté aucune modification au code mais j'obtiens maintenant l'erreur. Je pense que cela est dû au fait que ma méthode n'est ni robuste ni appropriée.

Toutes suggestions ou conseils sur la façon de diviser cette colonne de listes en colonnes séparées seraient très appréciés!

EDIT: Je pense que les méthodes .tolist () et .apply ne fonctionnent pas sur mon code car il s'agit d'une chaîne unicode, c'est-à-dire:

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

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

Les données sont importées de la base de données postgreSQL dans ce format. Avez-vous de l'aide ou des idées sur ce problème? y a-t-il un moyen de convertir l'unicode?

Llaffin
la source
J'ai répondu avec une solution légèrement différente, mais votre code devrait également fonctionner correctement. En utilisant mon exemple factice ci-dessous, cela fonctionne en utilisant pandas 0.18.1 si je laisse de côté la ilocpartie
joris
Cela iloc[:, :3]suppose-t-il qu'il y aura 3 éléments, et peut-être que les tranches de données plus récentes n'en ont que 1 ou 2 (par exemple, il n'y a pas de blike in index 8813)?
dwanderson le

Réponses:

168

Pour convertir la chaîne en un dict réel, vous pouvez le faire df['Pollutant Levels'].map(eval). Ensuite, la solution ci-dessous peut être utilisée pour convertir le dict en différentes colonnes.


En utilisant un petit exemple, vous pouvez utiliser .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

Pour le combiner avec le reste de la dataframe, vous pouvez concatles autres colonnes avec le résultat ci-dessus:

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

En utilisant votre code, cela fonctionne également si je laisse de côté la ilocpartie:

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
la source
2
J'utilise depuis pd.DataFrame(df[col].tolist())longtemps, je n'y ai jamais pensé apply(pd.Series). Très agréable.
ayhan
1
Je réalise maintenant le problème. Le .apply (pd.Series) ne fonctionne pas sur mon ensemble de données car la ligne entière est une chaîne unicode. C'est: u '{' a ':' 1 ',' b ':' 2 ',' c ':' 3 '} et non {u'a': '1', u'b ':' 2 ', u'c ':' 3 '} comme le montrent vos solutions. Le code ne peut donc pas le diviser en 3 colonnes reconnaissables.
llaffin le
2
@ayhan En fait, je l'ai testé, et l' DataFrame(df['col'].tolist())approche est un peu plus rapide que l'approche appliquer!
joris
3
@llaffin S'il s'agit d'une chaîne, vous pouvez la convertir en un dict réel avec df[col].map(eval)avant de le convertir en DataFrame
joris
2
Fonctionne parfaitement, mais est (beaucoup) plus lente que la nouvelle solution (2019)
fournie
85

Je sais que la question est assez ancienne, mais je suis arrivé ici à la recherche de réponses. Il existe actuellement un moyen meilleur (et plus rapide) de le faire en utilisant json_normalize:

import pandas as pd

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

Cela évite des fonctions d'application coûteuses ...

Lech Birek
la source
4
Hou la la! J'ai fait des fonctions d'application fastidieuses et déroutantes toute la journée dans Pandas sur des objets JSON, puis je suis tombé sur cette réponse et j'ai pensé: "Pas question, cela n'aurait pas pu être aussi simple!" Puis je l'ai essayé et ça l'a été. Merci beaucoup!
Emac
Le seul problème ici est qu'il ne semble pas copier sur d'autres colonnes sans json, ce qui signifie que si vous essayez de normaliser une ligne de valeurs json, vous devrez la copier et combiner les deux, toujours tellement mieux que mon itératif méthode. Cudos!
Mr.Drew le
pour cette solution, comment serait-il possible de sélectionner dynamiquement la liste des colonnes à normaliser? Les données transactionnelles que j'apporte à partir de .jsonfichiers proviennent de différentes sources et ce ne sont pas toujours les mêmes colonnes qui sont imbriquées. J'ai essayé de trouver un moyen de créer une liste de colonnes contenant des dictionnaires, mais je n'arrive pas à y parvenir
Callum Smyth
5
from pandas.io.json import json_normalize
Ramin Melikov le
Existe-t-il un moyen d'appliquer un préfixe aux dernières colonnes? J'ai remarqué qu'il y a des arguments comme meta_prefixet record_prefix. Bien que je ne puisse pas faire fonctionner cela avec mon dataframe (le dataframe final est correct dans mon cas mais je voudrais appliquer les préfixes).
J. Snow le
21

Essayez ceci: les données renvoyées par SQL doivent être converties en dictée. ou est-ce que "Pollutant Levels" c'est maintenantPollutants'

   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
Merlin
la source
13

La réponse de Merlin est meilleure et super facile, mais nous n'avons pas besoin d'une fonction lambda. L'évaluation du dictionnaire peut être ignorée en toute sécurité par l'une des deux méthodes suivantes, comme illustré ci-dessous:

Voie 1: Deux étapes

# 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

Voie 2: Les deux étapes ci-dessus peuvent être combinées en une seule fois:

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
la source
13

Je recommande fortement la méthode extraire la colonne 'Polluants':

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

c'est beaucoup plus rapide que

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

quand la taille de df est géante.

user9815968
la source
Ce serait formidable si vous pouviez expliquer comment / pourquoi cela fonctionne et est tellement mieux! pour moi, c'est toujours plus rapide et ~ 200 fois plus rapide une fois que vous obtenez plus de ~ 1000 lignes
Sam Mason
@SamMason lorsque vous faites applyl'ensemble de la trame de données est géré par des pandas, mais quand il s'agit de valuesjouer uniquement avec le numpy ndarraysqui est intrinsèquement plus rapide en raison du fait qu'il a des cimplémentations pures .
Sagar Kar
8

Vous pouvez utiliser joinavec pop+ tolist. La performance est comparable à celle concatavec drop+ tolist, mais certains peuvent trouver ce nettoyeur de syntaxe:

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

Analyse comparative avec d'autres méthodes:

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
la source
3

Une solution en ligne est la suivante:

>>> 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
la source
1

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

.. aurait analysé correctement le dict (en mettant chaque clé de dict dans une colonne df séparée et les valeurs de clé dans des lignes df), de sorte que les dicts ne seraient pas écrasés dans une seule colonne en premier lieu.

mirekphd
la source
0

J'ai concaténé ces étapes dans une méthode, vous devez passer uniquement le dataframe et la colonne qui contient le dict à développer:

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
la source
-1
df = pd.concat([df['a'], df.b.apply(pd.Series)], axis=1)
Siraj S.
la source