Dataframeセル内のリストを個別の行に分解する方法


93

リストを含むパンダのセルを、それらの各値の行に変換しようとしています。

だから、これを取る:

ここに画像の説明を入力してください

nearest_neighbors列の値をアンパックしてスタックし、各値が各opponentインデックス内の行になるようにしたい場合は、どうすればよいですか?このような操作を目的としたパンダメソッドはありますか?


希望する出力の例と、これまでに試みたことを教えてください。同様に、カットアンドペーストできるサンプルデータを提供すると、他の人があなたを助けるのが最も簡単になります。
dagrha

を使用pd.DataFrame(df.nearest_neighbors.values.tolist())してこのカラムを開梱pd.mergeし、他のカラムと 接着することができます。
hellpanderr

@helpanderr values.tolist()ここでは何もしないと思います。列は既にリストです
maxymoo


1
関連しているが、より詳細含まstackoverflow.com/questions/53218931/...
BEN_YO

回答:


54

以下のコードでは、行の反復を簡単にするために、最初にインデックスをリセットします。

外部リストの各要素がターゲットDataFrameの行であり、内部リストの各要素が列の1つであるリストのリストを作成します。このネストされたリストは最終的に連結され、目的のDataFrameを作成します。

Iは、使用lambdaの各要素の行を作成するリスト反復と共に機能nearest_neighbors関連とペアnameopponent

最後に、このリストから新しいDataFrameを作成します(元の列名を使用し、インデックスをnameおよびに戻しますopponent)。

df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                    'opponent': ['76ers', 'blazers', 'bobcats'], 
                    'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
      .set_index(['name', 'opponent']))

>>> df
                                                    nearest_neighbors
name       opponent                                                  
A.J. Price 76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           bobcats   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]

df.reset_index(inplace=True)
rows = []
_ = df.apply(lambda row: [rows.append([row['name'], row['opponent'], nn]) 
                         for nn in row.nearest_neighbors], axis=1)
df_new = pd.DataFrame(rows, columns=df.columns).set_index(['name', 'opponent'])

>>> df_new
                    nearest_neighbors
name       opponent                  
A.J. Price 76ers          Zach LaVine
           76ers           Jeremy Lin
           76ers        Nate Robinson
           76ers                Isaia
           blazers        Zach LaVine
           blazers         Jeremy Lin
           blazers      Nate Robinson
           blazers              Isaia
           bobcats        Zach LaVine
           bobcats         Jeremy Lin
           bobcats      Nate Robinson
           bobcats              Isaia

2017年6月を編集

別の方法は次のとおりです。

>>> (pd.melt(df.nearest_neighbors.apply(pd.Series).reset_index(), 
             id_vars=['name', 'opponent'],
             value_name='nearest_neighbors')
     .set_index(['name', 'opponent'])
     .drop('variable', axis=1)
     .dropna()
     .sort_index()
     )

apply(pd.Series)最も小さなフレームで問題ありませんが、妥当なサイズのフレームについては、よりパフォーマンスの高いソリューションを再検討する必要があります。コードでいつpandas apply()を使用する必要があるかを参照してください(より良い解決策は、列を最初にリスト化することです。)
cs95

2
リストのような列の分解は、explode()メソッドの追加によりパンダ0.25大幅に簡素化れました。私はここと同じdf設定を使用した例で回答を追加しまし
joelostblom

@joelostblom聞いてよかった。現在の使用例を追加していただきありがとうございます。
アレクサンダー

34

apply(pd.Series)andを使用しstack、次にreset_indexandto_frame

In [1803]: (df.nearest_neighbors.apply(pd.Series)
              .stack()
              .reset_index(level=2, drop=True)
              .to_frame('nearest_neighbors'))
Out[1803]:
                    nearest_neighbors
name       opponent
A.J. Price 76ers          Zach LaVine
           76ers           Jeremy Lin
           76ers        Nate Robinson
           76ers                Isaia
           blazers        Zach LaVine
           blazers         Jeremy Lin
           blazers      Nate Robinson
           blazers              Isaia
           bobcats        Zach LaVine
           bobcats         Jeremy Lin
           bobcats      Nate Robinson
           bobcats              Isaia

細部

In [1804]: df
Out[1804]:
                                                   nearest_neighbors
name       opponent
A.J. Price 76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           bobcats   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]

1
ソリューションの優雅さが大好きです!万が一、他のアプローチと比較してベンチマークしましたか?
rpyzh 2018

1
の結果はdf.nearest_neighbors.apply(pd.Series)私にとって非常に驚くべきものです。
Calum You

1
@rpyzhはい、それは非常にエレガントですが、悲惨なほど遅いです。
cs95

32
df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                    'opponent': ['76ers', 'blazers', 'bobcats'], 
                    'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
      .set_index(['name', 'opponent']))

df.explode('nearest_neighbors')

でる:

                    nearest_neighbors
name       opponent                  
A.J. Price 76ers          Zach LaVine
           76ers           Jeremy Lin
           76ers        Nate Robinson
           76ers                Isaia
           blazers        Zach LaVine
           blazers         Jeremy Lin
           blazers      Nate Robinson
           blazers              Isaia
           bobcats        Zach LaVine
           bobcats         Jeremy Lin
           bobcats      Nate Robinson
           bobcats              Isaia

2
これは単一の列(0.25以降)でのみ機能することに注意してください。より一般的なソリューションについては、ここここを参照してください。
cs95

16

これは本当に良い質問だと思います。Hiveで使用EXPLODEする場合、Pandasがデフォルトでこの機能を含める必要があるというケースがあると思います。私はおそらく、次のようなネストされたジェネレーター内包表記でリスト列を分解します。

pd.DataFrame({
    "name": i[0],
    "opponent": i[1],
    "nearest_neighbor": neighbour
    }
    for i, row in df.iterrows() for neighbour in row.nearest_neighbors
    ).set_index(["name", "opponent"])

このソリューションでは、リストアイテムの数を行ごとに異なるようにすることができます。
user1718097 2018

この方法で元のインデックスを保持する方法はありますか?
SummerEla 2018年

2
@SummerEla lolこれは本当に古い答えでした、私は今それをどのように行うかを示すように更新しました
maxymoo 2018年

1
@maxymooしかし、それでも素晴らしい質問です。更新してくれてありがとう!
SummerEla 2018年

私はこれが便利だと思ってパッケージに
Oren

11

最速の私が見つけた方法は、これまでとのデータフレームを拡張され.iloc、バック割り当てる平坦化されたターゲット列を。

通常の入力が与えられた場合(少し複製されます):

df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                    'opponent': ['76ers', 'blazers', 'bobcats'], 
                    'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
      .set_index(['name', 'opponent']))
df = pd.concat([df]*10)

df
Out[3]: 
                                                   nearest_neighbors
name       opponent                                                 
A.J. Price 76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           bobcats   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
...

以下の代替案が与えられた場合:

col_target = 'nearest_neighbors'

def extend_iloc():
    # Flatten columns of lists
    col_flat = [item for sublist in df[col_target] for item in sublist] 
    # Row numbers to repeat 
    lens = df[col_target].apply(len)
    vals = range(df.shape[0])
    ilocations = np.repeat(vals, lens)
    # Replicate rows and add flattened column of lists
    cols = [i for i,c in enumerate(df.columns) if c != col_target]
    new_df = df.iloc[ilocations, cols].copy()
    new_df[col_target] = col_flat
    return new_df

def melt():
    return (pd.melt(df[col_target].apply(pd.Series).reset_index(), 
             id_vars=['name', 'opponent'],
             value_name=col_target)
            .set_index(['name', 'opponent'])
            .drop('variable', axis=1)
            .dropna()
            .sort_index())

def stack_unstack():
    return (df[col_target].apply(pd.Series)
            .stack()
            .reset_index(level=2, drop=True)
            .to_frame(col_target))

私はそれextend_iloc()最速であると思います:

%timeit extend_iloc()
3.11 ms ± 544 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit melt()
22.5 ms ± 1.25 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit stack_unstack()
11.5 ms ± 410 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

素晴らしい評価
javadba

2
これをありがとう、それは本当に私を助けました。私はextend_ilocソリューションを使用しました cols = [c for c in df.columns if c != col_target] が、次cols = [i for i,c in enumerate(df.columns) if c != col_target] のようになるはず df.iloc[ilocations, cols].copy()です。
jdungan

ilocの提案に再度感謝します。私はここでそれがどのように機能するかについての詳細な説明を書きました: medium.com/@johnadungan/…。同様の課題を抱えているすべての人に役立つことを願っています。
jdungan

7

apply(pd.Series)を使用したより良い代替ソリューション:

df = pd.DataFrame({'listcol':[[1,2,3],[4,5,6]]})

# expand df.listcol into its own dataframe
tags = df['listcol'].apply(pd.Series)

# rename each variable is listcol
tags = tags.rename(columns = lambda x : 'listcol_' + str(x))

# join the tags dataframe back to the original dataframe
df = pd.concat([df[:], tags[:]], axis=1)

これは行ではなく列を展開します。
Oleg

@Oleg正しいですが、DataFrameをいつでも転置してから、pd.Seriesを他のほとんどの提案よりも簡単に適用できます
Philipp Schwarz

7

HiveのEXPLODE機能に似ています:

import copy

def pandas_explode(df, column_to_explode):
    """
    Similar to Hive's EXPLODE function, take a column with iterable elements, and flatten the iterable to one element 
    per observation in the output table

    :param df: A dataframe to explod
    :type df: pandas.DataFrame
    :param column_to_explode: 
    :type column_to_explode: str
    :return: An exploded data frame
    :rtype: pandas.DataFrame
    """

    # Create a list of new observations
    new_observations = list()

    # Iterate through existing observations
    for row in df.to_dict(orient='records'):

        # Take out the exploding iterable
        explode_values = row[column_to_explode]
        del row[column_to_explode]

        # Create a new observation for every entry in the exploding iterable & add all of the other columns
        for explode_value in explode_values:

            # Deep copy existing observation
            new_observation = copy.deepcopy(row)

            # Add one (newly flattened) value from exploding iterable
            new_observation[column_to_explode] = explode_value

            # Add to the list of new observations
            new_observations.append(new_observation)

    # Create a DataFrame
    return_df = pandas.DataFrame(new_observations)

    # Return
    return return_df

1
:私はこれを実行すると、私は次のエラーを取得NameError: global name 'copy' is not defined
frmsaul

4

だからこれらの答えはすべて良いですが、私は^本当に簡単なものを欲しがっていたので、これが私の貢献です:

def explode(series):
    return pd.Series([x for _list in series for x in _list])                               

それだけです。リストが「分解」される新しいシリーズが必要な場合は、これを使用してください。タコスの選択肢に対してvalue_counts()を実行する例を次に示します。

In [1]: my_df = pd.DataFrame(pd.Series([['a','b','c'],['b','c'],['c']]), columns=['tacos'])      
In [2]: my_df.head()                                                                               
Out[2]: 
   tacos
0  [a, b, c]
1     [b, c]
2        [c]

In [3]: explode(my_df['tacos']).value_counts()                                                     
Out[3]: 
c    3
b    2
a    1

2

これは、より大きなデータフレームの潜在的な最適化です。「爆発」フィールドにいくつかの等しい値がある場合、これはより速く実行されます。(データフレームがフィールド内の一意の値の数と比較して大きいほど、このコードのパフォーマンスは向上します。)

def lateral_explode(dataframe, fieldname): 
    temp_fieldname = fieldname + '_made_tuple_' 
    dataframe[temp_fieldname] = dataframe[fieldname].apply(tuple)       
    list_of_dataframes = []
    for values in dataframe[temp_fieldname].unique().tolist(): 
        list_of_dataframes.append(pd.DataFrame({
            temp_fieldname: [values] * len(values), 
            fieldname: list(values), 
        }))
    dataframe = dataframe[list(set(dataframe.columns) - set([fieldname]))]\ 
        .merge(pd.concat(list_of_dataframes), how='left', on=temp_fieldname) 
    del dataframe[temp_fieldname]

    return dataframe

1

Olegの.iloc答えを拡張して、すべてのリスト列を自動的にフラット化します。

def extend_iloc(df):
    cols_to_flatten = [colname for colname in df.columns if 
    isinstance(df.iloc[0][colname], list)]
    # Row numbers to repeat 
    lens = df[cols_to_flatten[0]].apply(len)
    vals = range(df.shape[0])
    ilocations = np.repeat(vals, lens)
    # Replicate rows and add flattened column of lists
    with_idxs = [(i, c) for (i, c) in enumerate(df.columns) if c not in cols_to_flatten]
    col_idxs = list(zip(*with_idxs)[0])
    new_df = df.iloc[ilocations, col_idxs].copy()

    # Flatten columns of lists
    for col_target in cols_to_flatten:
        col_flat = [item for sublist in df[col_target] for item in sublist]
        new_df[col_target] = col_flat

    return new_df

これは、各リスト列のリスト長が等しいと想定しています。


1

apply(pd.Series)を使用する代わりに、列をフラット化できます。これにより、パフォーマンスが向上します。

df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                'opponent': ['76ers', 'blazers', 'bobcats'], 
                'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
  .set_index(['name', 'opponent']))



%timeit (pd.DataFrame(df['nearest_neighbors'].values.tolist(), index = df.index)
           .stack()
           .reset_index(level = 2, drop=True).to_frame('nearest_neighbors'))

1.87 ms ± 9.74 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)


%timeit (df.nearest_neighbors.apply(pd.Series)
          .stack()
          .reset_index(level=2, drop=True)
          .to_frame('nearest_neighbors'))

2.73 ms ± 16.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

IndexError:レベルが多すぎます:私の例を試すと、インデックスには3レベルではなく2レベルしかありません
vinsent paramanantham

1
例に従って、reset_indexの「レベル」を変更する必要があります
suleep kumar
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.