Dzielenie słownika / listy wewnątrz kolumny Pandas na oddzielne kolumny


166

Mam dane zapisane w postgreSQLbazie danych. Pytam o te dane za pomocą Python2.7 i zamieniam je w Pandas DataFrame. Jednak w ostatniej kolumnie tej ramki danych znajduje się słownik (lub lista?) Wartości. DataFrame wygląda następująco:

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

Muszę podzielić tę kolumnę na osobne kolumny, aby DataFrame wyglądała następująco:

[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

Głównym problemem, który mam, jest to, że listy nie są tej samej długości. Ale wszystkie listy zawierają maksymalnie te same 3 wartości: a, b i c. I zawsze pojawiają się w tej samej kolejności (a pierwsza, b druga, c trzecia).

Poniższy kod UŻYWANY do działania i zwracania dokładnie tego, co chciałem (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)

Uruchomiłem ten kod w zeszłym tygodniu i działał dobrze. Ale teraz mój kod jest uszkodzony i otrzymuję ten błąd z wiersza [4]:

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

Nie wprowadziłem żadnych zmian w kodzie, ale teraz pojawia się błąd. Czuję, że jest to spowodowane tym, że moja metoda nie jest solidna ani właściwa.

Wszelkie sugestie lub wskazówki, jak podzielić tę kolumnę list na oddzielne kolumny, będą bardzo mile widziane!

EDYCJA: Myślę, że metody .tolist()i .apply nie działają na moim kodzie, ponieważ jest to jeden Unicodeciąg, tj .:

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

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

Dane są importowane z postgreSQLbazy danych w tym formacie. Jakaś pomoc lub pomysły dotyczące tego problemu? czy istnieje sposób na konwersję Unicode?

Odpowiedzi:


188

Aby przekonwertować ciąg na rzeczywisty dykt, możesz to zrobić df['Pollutant Levels'].map(eval). Następnie poniższe rozwiązanie może zostać użyte do konwersji dyktu na różne kolumny.


Posługując się małym przykładem, możesz użyć .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

Aby połączyć go z resztą ramki danych, możesz użyć concatinnych kolumn z powyższym wynikiem:

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

Używając twojego kodu, działa to również, jeśli pominę ilocczęść:

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

2
Używam od pd.DataFrame(df[col].tolist())dawna, nigdy o tym nie myślałem apply(pd.Series). Bardzo dobrze.
ayhan

1
Teraz zdaję sobie sprawę z problemu. .Apply (pd.Series) nie działa w moim zestawie danych, ponieważ cały wiersz jest jednym ciągiem znaków Unicode. To: u '{' a ':' 1 ',' b ':' 2 ',' c ':' 3 '}, a nie {u'a': '1', u'b ':' 2 ', u'c ':' 3 '}, jak pokazują twoje rozwiązania. Więc kod nie może podzielić go na 3 rozpoznawalne kolumny.
llaffin

3
@ayhan Właściwie przetestowałem to, a DataFrame(df['col'].tolist())podejście jest o wiele szybsze niż podejście zastosuj!
joris

3
@llaffin Jeśli jest ciągiem znaków, można przekonwertować do rzeczywistej dict z df[col].map(eval)przed przekształceniem go do DataFrame
joris

2
Działa idealnie, ale jest (znacznie) wolniejsze niż nowe rozwiązanie (2019) przesłane przez Lecha
Bireka

107

Wiem, że pytanie jest dość stare, ale przyszedłem tutaj, szukając odpowiedzi. Obecnie jest lepszy (i szybszy) sposób na zrobienie tego za pomocą json_normalize:

import pandas as pd

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

Pozwala to uniknąć kosztownych funkcji aplikacji ...


21

Spróbuj tego: dane zwrócone z SQL muszą zostać przekonwertowane na Dict. czy może być "Pollutant Levels" terazPollutants'

   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

13

Odpowiedź Merlina jest lepsza i super łatwa, ale nie potrzebujemy funkcji lambda. Ocenę słownika można bezpiecznie zignorować na jeden z dwóch poniższych sposobów, jak pokazano poniżej:

Sposób 1: Dwa kroki

# 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

Sposób 2: Powyższe dwa kroki można połączyć za jednym razem:

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

13

Gorąco polecam metodę wyodrębnienia kolumny „Zanieczyszczenia”:

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

jest znacznie szybszy niż

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

kiedy rozmiar df jest gigantyczny.


byłoby wspaniale, gdybyś mógł wyjaśnić, jak / dlaczego to działa i jest o wiele lepsze! dla mnie jest zawsze szybszy i ~ 200 razy szybszy, gdy zdobędziesz więcej niż ~ 1000 wierszy
Sam Mason

1
@SamMason, gdy robisz, applycała ramka danych jest zarządzana przez pandy, ale jeśli chodzi o valuesto, gra tylko z tym, numpy ndarraysco jest z natury szybsze ze względu na fakt, że ma czyste cimplementacje.
Sagar Kar

8

Możesz używać joinz pop+ tolist. Wydajność jest porównywalna concatz drop+ tolist, ale niektórzy mogą uznać tę składnię za czystszą:

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

Benchmarking innymi metodami:

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

4
  1. pd.json_normalize(df.Pollutants) jest znacznie szybszy niż df.Pollutants.apply(pd.Series)
    • Zobacz %%timeitponiżej. Dla 1M rzędów .json_normalizejest 47 razy szybszy niż .apply.
  2. Czy odczyt danych z pliku lub z obiektu zwróconego przez bazy danych lub automatycznie, to może nie być jasne, czy dictkolumna ma dictlub strtyp.
    • Jeśli słowniki w kolumnie są łańcuchami, należy je przekonwertować z powrotem na dicttyp przy użyciuast.literal_eval .
  3. Służy pd.json_normalizedo konwersji dicts, z keysnagłówkami i valueswierszami.
    • Posiada dodatkowe parametry (np. record_path& meta) Do obsługi zagnieżdżonych dicts.
  4. Służy pandas.DataFrame.joindo łączenia oryginalnej ramki DataFrame dfz kolumnami utworzonymi przy użyciupd.json_normalize
    • Jeśli indeks nie jest liczbami całkowitymi (jak w przykładzie), najpierw użyj go, df.reset_index()aby uzyskać indeks liczb całkowitych, przed wykonaniem normalizacji i łączenia.
  5. Na koniec użyj pandas.DataFrame.drop, aby usunąć niepotrzebną kolumnędicts
  • Uwaga, jeśli kolumna ma jakieś NaN, muszą być wypełnione pustymdict
import pandas as pd
from ast import literal_eval
import numpy as np

data = {'Station ID': [8809, 8810, 8811, 8812, 8813, 8814],
        'Pollutants': ['{"a": "46", "b": "3", "c": "12"}', '{"a": "36", "b": "5", "c": "8"}', '{"b": "2", "c": "7"}', '{"c": "11"}', '{"a": "82", "c": "15"}', np.nan]}

df = pd.DataFrame(data)

# display(df)
   Station ID                        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"}
5        8814                               NaN

# replace NaN with '{}' if the column is strings, otherwise replace with {}
# df.Pollutants = df.Pollutants.fillna('{}')  # if the NaN is in a column of strings
df.Pollutants = df.Pollutants.fillna({i: {} for i in df.index})  # if the column is not strings

# Convert the column of stringified dicts to dicts
# skip this line, if the column contains dicts
df.Pollutants = df.Pollutants.apply(literal_eval)

# reset the index if the index is not unique integers from 0 to n-1
# df.reset_index(inplace=True)  # uncomment if needed

# normalize the column of dictionaries and join it to df
df = df.join(pd.json_normalize(df.Pollutants))

# drop Pollutants
df.drop(columns=['Pollutants'], inplace=True)

# display(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
5        8814  NaN  NaN  NaN

%%timeit

# dataframe with 1M rows
dfb = pd.concat([df]*200000).reset_index(drop=True)

%%timeit
dfb.join(pd.json_normalize(dfb.Pollutants))
[out]:
5.44 s ± 32.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%%timeit
pd.concat([dfb.drop(columns=['Pollutants']), dfb.Pollutants.apply(pd.Series)], axis=1)
[out]:
4min 17s ± 2.44 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

1
Dzięki za to. Pracuję z 45 milionami wierszy i czekałem na zakończenie działania innego rozwiązania przez ponad 20 minut, zanim je przerwałem (prawdopodobnie działałoby przez wiele godzin). json_normalize działał w 2 minuty.
Brendan

3

Jedno rozwiązanie liniowe jest następujące:

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

1

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

.. poprawnie przeanalizowałby dyktando (umieszczając każdy klucz dykt w osobnej kolumnie df, a wartości kluczy w wierszach df), więc w pierwszej kolejności dykty nie zostałyby zgniecione w jednej kolumnie.


0

Połączyłem te kroki w metodzie, musisz przekazać tylko ramkę danych i kolumnę zawierającą dyktę do rozwinięcia:

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


0
>>> df

   Station ID                        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"}

porównanie szybkości dla dużego zbioru danych zawierającego 10 milionów wierszy

>>> df = pd.concat([df]*100000).reset_index(drop=True)
>>> df = pd.concat([df]*20).reset_index(drop=True)
>>> print(df.shape)
(10000000, 2)
def apply_drop(df):
    return df.join(df['Pollutants'].apply(pd.Series)).drop('Pollutants', axis=1)  

def json_normalise_drop(df):
    return df.join(pd.json_normalize(df.Pollutants)).drop('Pollutants', axis=1)  

def tolist_drop(df):
    return df.join(pd.DataFrame(df['Pollutants'].tolist())).drop('Pollutants', axis=1)  

def vlues_tolist_drop(df):
    return df.join(pd.DataFrame(df['Pollutants'].values.tolist())).drop('Pollutants', axis=1)  

def pop_tolist(df):
    return df.join(pd.DataFrame(df.pop('Pollutants').tolist()))  

def pop_values_tolist(df):
    return df.join(pd.DataFrame(df.pop('Pollutants').values.tolist()))

>>> %timeit apply_drop(df.copy())
1 loop, best of 3: 53min 20s per loop
>>> %timeit json_normalise_drop(df.copy())
1 loop, best of 3: 54.9 s per loop
>>> %timeit tolist_drop(df.copy())
1 loop, best of 3: 6.62 s per loop
>>> %timeit vlues_tolist_drop(df.copy())
1 loop, best of 3: 6.63 s per loop
>>> %timeit pop_tolist(df.copy())
1 loop, best of 3: 5.99 s per loop
>>> %timeit pop_values_tolist(df.copy())
1 loop, best of 3: 5.94 s per loop
+---------------------+-----------+
| apply_drop          | 53min 20s |
| json_normalise_drop |    54.9 s |
| tolist_drop         |    6.62 s |
| vlues_tolist_drop   |    6.63 s |
| pop_tolist          |    5.99 s |
| pop_values_tolist   |    5.94 s |
+---------------------+-----------+

df.join(pd.DataFrame(df.pop('Pollutants').values.tolist())) jest najszybszy

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.