Aufteilen des Wörterbuchs / der Liste innerhalb einer Pandas-Spalte in separate Spalten

145

Ich habe Daten in einer PostgreSQL-Datenbank gespeichert. Ich frage diese Daten mit Python2.7 ab und verwandle sie in einen Pandas DataFrame. Die letzte Spalte dieses Datenrahmens enthält jedoch ein Wörterbuch (oder eine Liste?) Mit Werten. Der DataFrame sieht folgendermaßen aus:

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

Ich muss diese Spalte in separate Spalten aufteilen, damit der DataFrame folgendermaßen aussieht:

[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

Das Hauptproblem, das ich habe, ist, dass die Listen nicht gleich lang sind. Alle Listen enthalten jedoch nur bis zu den gleichen 3 Werten: a, b und c. Und sie erscheinen immer in derselben Reihenfolge (a erste, b zweite, c dritte).

Der folgende Code wurde verwendet, um zu arbeiten und genau das zurückzugeben, was ich wollte (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)

Ich habe diesen Code erst letzte Woche ausgeführt und er hat gut funktioniert. Aber jetzt ist mein Code kaputt und ich bekomme diesen Fehler aus Zeile [4]:

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

Ich habe keine Änderungen am Code vorgenommen, erhalte aber jetzt den Fehler. Ich denke, das liegt daran, dass meine Methode nicht robust oder richtig ist.

Anregungen oder Anleitungen zum Aufteilen dieser Listenspalte in separate Spalten sind sehr willkommen!

EDIT: Ich denke, die Methoden .tolist () und .apply funktionieren nicht mit meinem Code, da es sich um eine Unicode-Zeichenfolge handelt, dh:

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

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

Die Daten werden in diesem Format aus der postgreSQL-Datenbank importiert. Hilfe oder Ideen zu diesem Thema? Gibt es eine Möglichkeit, den Unicode zu konvertieren?

Laffin
quelle
Ich antwortete mit einer etwas anderen Lösung, aber Ihr Code sollte eigentlich auch gut funktionieren. Mit meinem Dummy-Beispiel unten funktioniert dies mit Pandas 0.18.1, wenn ich den ilocTeil
weglasse
Ist ein Teil davon, dass iloc[:, :3]davon ausgegangen wird, dass es 3 Elemente gibt, und dass neuere Datenscheiben möglicherweise nur 1 oder 2 haben (z. B. gibt es zufällig kein bLike in index 8813)?
Dwanderson

Antworten:

166

Sie können die Zeichenfolge in ein tatsächliches Diktat konvertieren df['Pollutant Levels'].map(eval). Anschließend kann die folgende Lösung verwendet werden, um das Diktat in verschiedene Spalten zu konvertieren.


Anhand eines kleinen Beispiels können Sie Folgendes verwenden .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

Um es mit dem Rest des Datenrahmens zu kombinieren, können Sie concatdie anderen Spalten mit dem obigen Ergebnis verwenden:

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

Mit Ihrem Code funktioniert dies auch, wenn ich das ilocTeil weglasse:

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
quelle
2
Ich habe pd.DataFrame(df[col].tolist())für eine lange Zeit verwendet, nie darüber nachgedacht apply(pd.Series). Sehr schön.
Ayhan
1
Ich erkenne jetzt das Problem. Die .apply (pd.Series) funktioniert in meinem Dataset nicht, da die gesamte Zeile eine Unicode-Zeichenfolge ist. Es ist: u '{' a ':' 1 ',' b ':' 2 ',' c ':' 3 '} und nicht {u'a': '1', u'b ':' 2 ', u'c ':' 3 '} wie Ihre Lösungen zeigen. Der Code kann ihn also nicht in 3 erkennbare Spalten aufteilen.
Laffin
2
@ayhan Eigentlich getestet, und der DataFrame(df['col'].tolist())Ansatz ist ziemlich viel schneller als der Apply- Ansatz!
Joris
3
@llaffin Wenn es sich um eine Zeichenfolge handelt, können Sie diese in ein tatsächliches df[col].map(eval)Diktat konvertieren, bevor Sie sie in einen DataFrame konvertieren
joris
2
Funktioniert perfekt, ist aber (viel) langsamer als die neue Lösung (2019) von Lech Birek stackoverflow.com/a/55355928/2721710
drasc
84

Ich weiß, dass die Frage ziemlich alt ist, aber ich bin hierher gekommen, um nach Antworten zu suchen. Es gibt tatsächlich einen besseren (und schnelleren) Weg, dies zu tun, indem man json_normalize:

import pandas as pd

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

Dies vermeidet kostspielige Anwendungsfunktionen ...

Lech Birek
quelle
4
Beeindruckend! Ich habe den ganzen Tag lang mühsame und verwirrende Anwendungsfunktionen in Pandas für JSON-Objekte ausgeführt, bin dann auf diese Antwort gestoßen und dachte: "Auf keinen Fall, es hätte nicht so einfach sein können!" Dann habe ich es versucht und es war. Vielen Dank!
Emac
Das einzige Problem hierbei ist, dass es nicht ohne json über andere Spalten zu kopieren scheint. Wenn Sie also versuchen, eine Zeile mit json-Werten zu normalisieren, müssen Sie es kopieren und die beiden kombinieren, immer noch viel besser als meine Iteration Methode. Ein dickes Lob!
Mr.Drew
Wie wäre es für diese Lösung möglich, die Liste der zu normalisierenden Spalten dynamisch auszuwählen? Die Transaktionsdaten, die ich aus .jsonDateien einbringe, stammen aus verschiedenen Quellen und es sind nicht immer dieselben Spalten, die verschachtelt sind. Ich habe versucht, einen Weg zu finden, um eine Liste von Spalten zu erstellen, die Diktate enthalten, aber es scheint nicht zu funktionieren
Callum Smyth
5
from pandas.io.json import json_normalize
Ramin Melikov
Gibt es eine Möglichkeit, ein Präfix auf die letzten Spalten anzuwenden? Mir ist aufgefallen, dass es Argumente wie meta_prefixund gibt record_prefix. Ich kann das zwar nicht mit meinem Datenrahmen zum Laufen bringen (der endgültige Datenrahmen ist in meinem Fall korrekt, aber ich möchte die Präfixe anwenden).
J. Snow
21

Versuchen Sie Folgendes: Die von SQL zurückgegebenen Daten müssen in ein Dict konvertiert werden. oder könnte es "Pollutant Levels" jetzt seinPollutants'

   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
quelle
13

Merlins Antwort ist besser und super einfach, aber wir brauchen keine Lambda-Funktion. Die Auswertung des Wörterbuchs kann auf zwei der folgenden Arten ignoriert werden:

Weg 1: Zwei Schritte

# 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

Weg 2: Die beiden oben genannten Schritte können auf einmal kombiniert werden:

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
quelle
12

Ich empfehle dringend die Methode, die Spalte 'Schadstoffe' zu extrahieren:

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

es ist viel schneller als

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

wenn die Größe von df riesig ist.

user9815968
quelle
wäre toll wenn du erklären könntest wie / warum das funktioniert und so viel besser ist! für mich ist es immer schneller und ~ 200 mal schneller, wenn Sie mehr als ~ 1000 Zeilen erhalten
Sam Mason
@SamMason, wenn Sie dies tun, wird applyder gesamte Datenrahmen von Pandas verwaltet, aber wenn es darum valuesgeht, spielt es nur mit dem, numpy ndarrayswas aufgrund der Tatsache, dass es reine cImplementierungen hat, wesentlich schneller ist .
Sagar Kar
8

Sie können joinmit pop+ verwenden tolist. Die Leistung ist vergleichbar concatmit drop+ tolist, aber einige finden diese Syntax möglicherweise sauberer:

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

Benchmarking mit anderen Methoden:

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
quelle
3

Eine einzeilige Lösung folgt:

>>> 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
quelle
1

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

.. hätte das Diktat richtig analysiert (indem jeder Diktatschlüssel in eine separate df-Spalte und die Schlüsselwerte in df-Zeilen eingefügt wurden), sodass die Diktate überhaupt nicht in eine einzelne Spalte gequetscht würden.

mirekphd
quelle
0

Ich habe diese Schritte in einer Methode verkettet. Sie müssen nur den Datenrahmen und die Spalte übergeben, die das zu erweiternde Diktat enthält:

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