テキスト文字列の1列にコンマ区切りの値が含まれるpandas dataframe
があります。各CSVフィールドを分割し、エントリごとに新しい行を作成します(CSVはクリーンで、「、」でのみ分割する必要があると仮定します)。たとえば、a
はb
になります。
In [7]: a
Out[7]:
var1 var2
0 a,b,c 1
1 d,e,f 2
In [8]: b
Out[8]:
var1 var2
0 a 1
1 b 1
2 c 1
3 d 2
4 e 2
5 f 2
これまで、さまざまな単純な関数を試してきましたが、.apply
メソッドは、軸で使用される場合、戻り値として1行のみを受け入れるようであり、.transform
を取得できません。どんな提案でも大歓迎です!
サンプルデータ:
from pandas import DataFrame
import numpy as np
a = DataFrame([{'var1': 'a,b,c', 'var2': 1},
{'var1': 'd,e,f', 'var2': 2}])
b = DataFrame([{'var1': 'a', 'var2': 1},
{'var1': 'b', 'var2': 1},
{'var1': 'c', 'var2': 1},
{'var1': 'd', 'var2': 2},
{'var1': 'e', 'var2': 2},
{'var1': 'f', 'var2': 2}])
Numpyを実行するとDataFrameメタデータが失われるため、これが機能しないことはわかっていますが、私がやろうとしたことの意味がわかるはずです。
def fun(row):
letters = row['var1']
letters = letters.split(',')
out = np.array([row] * len(letters))
out['var1'] = letters
a['idx'] = range(a.shape[0])
z = a.groupby('idx')
z.transform(fun)
このようなものはどうですか:
In [55]: pd.concat([Series(row['var2'], row['var1'].split(','))
for _, row in a.iterrows()]).reset_index()
Out[55]:
index 0
0 a 1
1 b 1
2 c 1
3 d 2
4 e 2
5 f 2
次に、列の名前を変更する必要があります
UPDATE2:複数のnormal
および複数のlist
列で機能する、より一般的なベクトル化関数
def explode(df, lst_cols, fill_value='', preserve_index=False):
# make sure `lst_cols` is list-alike
if (lst_cols is not None
and len(lst_cols) > 0
and not isinstance(lst_cols, (list, Tuple, np.ndarray, pd.Series))):
lst_cols = [lst_cols]
# all columns except `lst_cols`
idx_cols = df.columns.difference(lst_cols)
# calculate lengths of lists
lens = df[lst_cols[0]].str.len()
# preserve original index values
idx = np.repeat(df.index.values, lens)
# create "exploded" DF
res = (pd.DataFrame({
col:np.repeat(df[col].values, lens)
for col in idx_cols},
index=idx)
.assign(**{col:np.concatenate(df.loc[lens>0, col].values)
for col in lst_cols}))
# append those rows that have empty lists
if (lens == 0).any():
# at least one list in cells is empty
res = (res.append(df.loc[lens==0, idx_cols], sort=False)
.fillna(fill_value))
# revert the original index order
res = res.sort_index()
# reset index if requested
if not preserve_index:
res = res.reset_index(drop=True)
return res
デモ:
複数のlist
列-すべてのlist
列には、各行で同じ要素数が必要です。
In [134]: df
Out[134]:
aaa myid num text
0 10 1 [1, 2, 3] [aa, bb, cc]
1 11 2 [] []
2 12 3 [1, 2] [cc, dd]
3 13 4 [] []
In [135]: explode(df, ['num','text'], fill_value='')
Out[135]:
aaa myid num text
0 10 1 1 aa
1 10 1 2 bb
2 10 1 3 cc
3 11 2
4 12 3 1 cc
5 12 3 2 dd
6 13 4
元のインデックス値を保持します:
In [136]: explode(df, ['num','text'], fill_value='', preserve_index=True)
Out[136]:
aaa myid num text
0 10 1 1 aa
0 10 1 2 bb
0 10 1 3 cc
1 11 2
2 12 3 1 cc
2 12 3 2 dd
3 13 4
セットアップ:
df = pd.DataFrame({
'aaa': {0: 10, 1: 11, 2: 12, 3: 13},
'myid': {0: 1, 1: 2, 2: 3, 3: 4},
'num': {0: [1, 2, 3], 1: [], 2: [1, 2], 3: []},
'text': {0: ['aa', 'bb', 'cc'], 1: [], 2: ['cc', 'dd'], 3: []}
})
CSV列:
In [46]: df
Out[46]:
var1 var2 var3
0 a,b,c 1 XX
1 d,e,f,x,y 2 ZZ
In [47]: explode(df.assign(var1=df.var1.str.split(',')), 'var1')
Out[47]:
var1 var2 var3
0 a 1 XX
1 b 1 XX
2 c 1 XX
3 d 2 ZZ
4 e 2 ZZ
5 f 2 ZZ
6 x 2 ZZ
7 y 2 ZZ
この小さなトリックを使用して、CSVのような列をlist
列に変換できます。
In [48]: df.assign(var1=df.var1.str.split(','))
Out[48]:
var1 var2 var3
0 [a, b, c] 1 XX
1 [d, e, f, x, y] 2 ZZ
UPDATE:一般的なベクトル化アプローチ(複数列でも機能します):
元のDF:
In [177]: df
Out[177]:
var1 var2 var3
0 a,b,c 1 XX
1 d,e,f,x,y 2 ZZ
解決策:
最初にCSV文字列をリストに変換しましょう:
In [178]: lst_col = 'var1'
In [179]: x = df.assign(**{lst_col:df[lst_col].str.split(',')})
In [180]: x
Out[180]:
var1 var2 var3
0 [a, b, c] 1 XX
1 [d, e, f, x, y] 2 ZZ
これを行うことができます:
In [181]: pd.DataFrame({
...: col:np.repeat(x[col].values, x[lst_col].str.len())
...: for col in x.columns.difference([lst_col])
...: }).assign(**{lst_col:np.concatenate(x[lst_col].values)})[x.columns.tolist()]
...:
Out[181]:
var1 var2 var3
0 a 1 XX
1 b 1 XX
2 c 1 XX
3 d 2 ZZ
4 e 2 ZZ
5 f 2 ZZ
6 x 2 ZZ
7 y 2 ZZ
古い答え:
@ AFinkelstein solution に触発され、2つ以上の列を使用してDFに適用でき、AFinkelsteinのソリューションと同じくらい速く、ほぼ同じくらい速く、より一般化したかった):
In [2]: df = pd.DataFrame(
...: [{'var1': 'a,b,c', 'var2': 1, 'var3': 'XX'},
...: {'var1': 'd,e,f,x,y', 'var2': 2, 'var3': 'ZZ'}]
...: )
In [3]: df
Out[3]:
var1 var2 var3
0 a,b,c 1 XX
1 d,e,f,x,y 2 ZZ
In [4]: (df.set_index(df.columns.drop('var1',1).tolist())
...: .var1.str.split(',', expand=True)
...: .stack()
...: .reset_index()
...: .rename(columns={0:'var1'})
...: .loc[:, df.columns]
...: )
Out[4]:
var1 var2 var3
0 a 1 XX
1 b 1 XX
2 c 1 XX
3 d 2 ZZ
4 e 2 ZZ
5 f 2 ZZ
6 x 2 ZZ
7 y 2 ZZ
受け入れられた答えよりも速く何かを見つけるための苦しい実験の後、私はこれを機能させました。試したデータセットでは、約100倍高速で実行されました。
誰かがこれをよりエレガントにする方法を知っているなら、ぜひ私のコードを修正してください。保持したい他の列をインデックスとして設定し、インデックスをリセットして列の名前を変更せずに機能する方法を見つけることができませんでしたが、他に機能するものがあると思います。
b = DataFrame(a.var1.str.split(',').tolist(), index=a.var2).stack()
b = b.reset_index()[[0, 'var2']] # var1 variable is currently labeled 0
b.columns = ['var1', 'var2'] # renaming var1
この共通タスクの 私が書いた関数 を示します。 Series
/stack
メソッドよりも効率的です。列の順序と名前は保持されます。
def tidy_split(df, column, sep='|', keep=False):
"""
Split the values of a column and expand so the new DataFrame has one split
value per row. Filters rows where the column is missing.
Params
------
df : pandas.DataFrame
dataframe with the column to split and expand
column : str
the column to split and expand
sep : str
the string used to split the column's values
keep : bool
whether to retain the presplit value as it's own row
Returns
-------
pandas.DataFrame
Returns a dataframe with the same columns as `df`.
"""
indexes = list()
new_values = list()
df = df.dropna(subset=[column])
for i, presplit in enumerate(df[column].astype(str)):
values = presplit.split(sep)
if keep and len(values) > 1:
indexes.append(i)
new_values.append(presplit)
for value in values:
indexes.append(i)
new_values.append(value)
new_df = df.iloc[indexes, :].copy()
new_df[column] = new_values
return new_df
この関数を使用すると、 元の質問 は次のように簡単です:
tidy_split(a, 'var1', sep=',')
pandas:列内のテキストを複数の行に分割するにはどうすればよいですか?
できること:
>> a=pd.DataFrame({"var1":"a,b,c d,e,f".split(),"var2":[1,2]})
>> s = a.var1.str.split(",").apply(pd.Series, 1).stack()
>> s.index = s.index.droplevel(-1)
>> del a['var1']
>> a.join(s)
var2 var1
0 1 a
0 1 b
0 1 c
1 2 d
1 2 e
1 2 f
import pandas as pd
import numpy as np
def explode_str(df, col, sep):
s = df[col]
i = np.arange(len(s)).repeat(s.str.count(sep) + 1)
return df.iloc[i].assign(**{col: sep.join(s).split(sep)})
def explode_list(df, col):
s = df[col]
i = np.arange(len(s)).repeat(s.str.len())
return df.iloc[i].assign(**{col: np.concatenate(s)})
explode_str(a, 'var1', ',')
var1 var2
0 a 1
0 b 1
0 c 1
1 d 2
1 e 2
1 f 2
リストを持つ新しいデータフレームd
を作成しましょう
d = a.assign(var1=lambda d: d.var1.str.split(','))
explode_list(d, 'var1')
var1 var2
0 a 1
0 b 1
0 c 1
1 d 2
1 e 2
1 f 2
np.arange
をrepeat
とともに使用して、iloc
で使用できるデータフレームインデックス位置を生成します。
loc
を使用しないのはなぜですか?インデックスは一意ではない可能性があるため、loc
を使用すると、クエリされたインデックスに一致するすべての行が返されます。
values
属性を使用してスライスしないのですか?values
を呼び出すときに、データフレーム全体が1つの凝集した「ブロック」にある場合、Pandasは「ブロック」である配列のビューを返します。それ以外の場合、Pandasは新しい配列を一緒に作成する必要があります。コブリングする場合、その配列は均一なdtypeでなければなりません。多くの場合、object
であるdtypeを持つ配列を返すことを意味します。 iloc
属性をスライスする代わりにvalues
を使用することにより、これに対処する必要がなくなります。
assign
を使用するのですか?爆発するのと同じ列名を使用してassign
を使用すると、既存の列を上書きし、データフレーム内での位置を維持します。
繰り返し位置でiloc
を使用することにより、結果のインデックスは同じ繰り返しパターンを示します。リストまたは文字列の各要素に対して1回繰り返します。
これはreset_index(drop=True)
でリセットできます
文字列を途中で分割する必要はありません。したがって、代わりにsep
引数の出現回数をカウントします。分割する場合、結果のリストの長さはセパレーターの数よりも1つ多くなります。
次に、そのsep
からjoin
を使用して、文字列をsplit
にします。
def explode_str(df, col, sep):
s = df[col]
i = np.arange(len(s)).repeat(s.str.count(sep) + 1)
return df.iloc[i].assign(**{col: sep.join(s).split(sep)})
文字列と同様ですが、すでに分割されているため、sep
の出現をカウントする必要はありません。
Numpyのconcatenate
を使用して、リストをまとめます。
import pandas as pd
import numpy as np
def explode_list(df, col):
s = df[col]
i = np.arange(len(s)).repeat(s.str.len())
return df.iloc[i].assign(**{col: np.concatenate(s)})
SeriesおよびDataFrameメソッドは、 .explode()
リストを分解するリストを定義します行。 リストのような列の展開 のドキュメントセクションを参照してください。
コンマで区切られた文字列のリストがあるため、文字列をコンマで分割して要素のリストを取得し、その列でexplode
を呼び出します。
df = pd.DataFrame({'var1': ['a,b,c', 'd,e,f'], 'var2': [1, 2]})
df
var1 var2
0 a,b,c 1
1 d,e,f 2
df.assign(var1=df['var1'].str.split(',')).explode('var1')
var1 var2
0 a 1
0 b 1
0 c 1
1 d 2
1 e 2
1 f 2
explode
は単一の列でのみ機能することに注意してください(今のところ)。
NaNと空のリストは、あなたがそれを正しくするためにフープを飛び越える必要なく、彼らが受けるに値する治療を受けます。
df = pd.DataFrame({'var1': ['d,e,f', '', np.nan], 'var2': [1, 2, 3]})
df
var1 var2
0 d,e,f 1
1 2
2 NaN 3
df['var1'].str.split(',')
0 [d, e, f]
1 []
2 NaN
df.assign(var1=df['var1'].str.split(',')).explode('var1')
var1 var2
0 d 1
0 e 1
0 f 1
1 2 # empty list entry becomes empty string after exploding
2 NaN 3 # NaN left un-touched
これはravel
+ repeat
ベースのソリューション(空のリストを完全に無視し、NaNでチョークする)よりも深刻な利点です。 。
任意の数の列を持つデータフレームのソリューションを考え出しました(ただし、一度に1つの列のエントリのみを分離します)。
def splitDataFrameList(df,target_column,separator):
''' df = dataframe to split,
target_column = the column containing the values to split
separator = the symbol used to perform the split
returns: a dataframe with each entry for the target column separated, with each element moved into a new row.
The values in the other columns are duplicated across the newly divided rows.
'''
def splitListToRows(row,row_accumulator,target_column,separator):
split_row = row[target_column].split(separator)
for s in split_row:
new_row = row.to_dict()
new_row[target_column] = s
row_accumulator.append(new_row)
new_rows = []
df.apply(splitListToRows,axis=1,args = (new_rows,target_column,separator))
new_df = pandas.DataFrame(new_rows)
return new_df
pandas split
アクセサーからのstr
メソッドを使用し、NumPyを使用して各行を単一の配列にフラット化するかなり簡単なメッセージを次に示します。
対応する値は、np.repeat
を使用して非分割列を正しい回数繰り返すことにより取得されます。
var1 = df.var1.str.split(',', expand=True).values.ravel()
var2 = np.repeat(df.var2.values, len(var1) / len(df))
pd.DataFrame({'var1': var1,
'var2': var2})
var1 var2
0 a 1
1 b 1
2 c 1
3 d 2
4 e 2
5 f 2
文字列関数splitは、オプションのブール引数 'expand'を取ることができます。
この引数を使用したソリューションは次のとおりです。
(a.var1
.str.split(",",expand=True)
.set_index(a.var2)
.stack()
.reset_index(level=1, drop=True)
.reset_index()
.rename(columns={0:"var1"}))
私はリストを爆発させるためにさまざまな方法を使用してメモリ不足の経験に苦労してきたので、投票する答えを決めるのに役立つベンチマークを用意しました。リストの長さとリストの数の割合を変えて5つのシナリオをテストしました。以下の結果を共有します。
時間:(少ない方が良い、クリックして大きなバージョンを表示する)
ピークメモリ使用量:(少ないほど良い)
結論:
詳細(関数とベンチマークコード)は、この中にあります GitHub Gist 。ベンチマークの問題は単純化されており、文字列のリストへの分割は含まれていないことに注意してください-ほとんどのソリューションは同様の方法で実行されました。
データフレームの構造を変更せずに、データフレームを分割および展開する可能性があります
入力:
var1 var2
0 a,b,c 1
1 d,e,f 2
#Get the indexes which are repetative with the split
df = df.reindex(df.index.repeat(df.var1.str.split(',').apply(len)))
#Assign the split values to dataframe column
df['var1'] = np.hstack(df['var1'].drop_duplicates().str.split(','))
でる:
var1 var2
0 a 1
0 b 1
0 c 1
1 d 2
1 e 2
1 f 2
優れた@DMulliganの solution に基づいて、データフレームの列を複数の行に分割し、元のデータフレームにマージする汎用のベクトル化(ループなし)関数を次に示します。また、この answer の優れた汎用change_column_order
関数も使用します。
def change_column_order(df, col_name, index):
cols = df.columns.tolist()
cols.remove(col_name)
cols.insert(index, col_name)
return df[cols]
def split_df(dataframe, col_name, sep):
orig_col_index = dataframe.columns.tolist().index(col_name)
orig_index_name = dataframe.index.name
orig_columns = dataframe.columns
dataframe = dataframe.reset_index() # we need a natural 0-based index for proper merge
index_col_name = (set(dataframe.columns) - set(orig_columns)).pop()
df_split = pd.DataFrame(
pd.DataFrame(dataframe[col_name].str.split(sep).tolist())
.stack().reset_index(level=1, drop=1), columns=[col_name])
df = dataframe.drop(col_name, axis=1)
df = pd.merge(df, df_split, left_index=True, right_index=True, how='inner')
df = df.set_index(index_col_name)
df.index.name = orig_index_name
# merge adds the column to the last place, so we need to move it back
return change_column_order(df, col_name, orig_col_index)
例:
df = pd.DataFrame([['a:b', 1, 4], ['c:d', 2, 5], ['e:f:g:h', 3, 6]],
columns=['Name', 'A', 'B'], index=[10, 12, 13])
df
Name A B
10 a:b 1 4
12 c:d 2 5
13 e:f:g:h 3 6
split_df(df, 'Name', ':')
Name A B
10 a 1 4
10 b 1 4
12 c 2 5
12 d 2 5
13 e 3 6
13 f 3 6
13 g 3 6
13 h 3 6
元のインデックスと列の順序が保持されることに注意してください。また、非順次インデックスを持つデータフレームでも機能します。
multiIndexをサポートしたMaxUの回答のアップグレード
def explode(df, lst_cols, fill_value='', preserve_index=False):
"""
usage:
In [134]: df
Out[134]:
aaa myid num text
0 10 1 [1, 2, 3] [aa, bb, cc]
1 11 2 [] []
2 12 3 [1, 2] [cc, dd]
3 13 4 [] []
In [135]: explode(df, ['num','text'], fill_value='')
Out[135]:
aaa myid num text
0 10 1 1 aa
1 10 1 2 bb
2 10 1 3 cc
3 11 2
4 12 3 1 cc
5 12 3 2 dd
6 13 4
"""
# make sure `lst_cols` is list-alike
if (lst_cols is not None
and len(lst_cols) > 0
and not isinstance(lst_cols, (list, Tuple, np.ndarray, pd.Series))):
lst_cols = [lst_cols]
# all columns except `lst_cols`
idx_cols = df.columns.difference(lst_cols)
# calculate lengths of lists
lens = df[lst_cols[0]].str.len()
# preserve original index values
idx = np.repeat(df.index.values, lens)
res = (pd.DataFrame({
col:np.repeat(df[col].values, lens)
for col in idx_cols},
index=idx)
.assign(**{col:np.concatenate(df.loc[lens>0, col].values)
for col in lst_cols}))
# append those rows that have empty lists
if (lens == 0).any():
# at least one list in cells is empty
res = (res.append(df.loc[lens==0, idx_cols], sort=False)
.fillna(fill_value))
# revert the original index order
res = res.sort_index()
# reset index if requested
if not preserve_index:
res = res.reset_index(drop=True)
# if original index is MultiIndex build the dataframe from the multiindex
# create "exploded" DF
if isinstance(df.index, pd.MultiIndex):
res = res.reindex(
index=pd.MultiIndex.from_tuples(
res.index,
names=['number', 'color']
)
)
return res
pythonコピーパッケージを使用する別のソリューション
import copy
new_observations = list()
def pandas_explode(df, column_to_explode):
new_observations = list()
for row in df.to_dict(orient='records'):
explode_values = row[column_to_explode]
del row[column_to_explode]
if type(explode_values) is list or type(explode_values) is Tuple:
for explode_value in explode_values:
new_observation = copy.deepcopy(row)
new_observation[column_to_explode] = explode_value
new_observations.append(new_observation)
else:
new_observation = copy.deepcopy(row)
new_observation[column_to_explode] = explode_values
new_observations.append(new_observation)
return_df = pd.DataFrame(new_observations)
return return_df
df = pandas_explode(df, column_name)
ここには多くの答えがありますが、組み込みのpandas explode関数について誰も言及していないことに驚いています。以下のリンクをご覧ください: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.explode.html#pandas.DataFrame.explode
何らかの理由で私はその機能にアクセスできなかったため、次のコードを使用しました。
import pandas_explode
pandas_explode.patch()
df_zlp_people_cnt3 = df_zlp_people_cnt2.explode('people')
上記は私のデータのサンプルです。ご覧のとおり、people列には一連の人がおり、爆発させようとしていました。私が与えたコードはリスト型のデータに対して機能します。したがって、コンマ区切りのテキストデータをリスト形式に変換してください。また、私のコードは組み込み関数を使用しているため、カスタム/適用関数よりもはるかに高速です。
注:pandas_explodeをpipでインストールする必要がある場合があります。
この問題に対する次の解決策を思いつきました。
def iter_var1(d):
for _, row in d.iterrows():
for v in row["var1"].split(","):
yield (v, row["var2"])
new_a = DataFrame.from_records([i for i in iter_var1(a)],
columns=["var1", "var2"])
上記のjilnの優れた答えを使用しましたが、拡張して複数の列を分割する必要がありました。私は共有すると思いました。
def splitDataFrameList(df,target_column,separator):
''' df = dataframe to split,
target_column = the column containing the values to split
separator = the symbol used to perform the split
returns: a dataframe with each entry for the target column separated, with each element moved into a new row.
The values in the other columns are duplicated across the newly divided rows.
'''
def splitListToRows(row, row_accumulator, target_columns, separator):
split_rows = []
for target_column in target_columns:
split_rows.append(row[target_column].split(separator))
# Seperate for multiple columns
for i in range(len(split_rows[0])):
new_row = row.to_dict()
for j in range(len(split_rows)):
new_row[target_columns[j]] = split_rows[j][i]
row_accumulator.append(new_row)
new_rows = []
df.apply(splitListToRows,axis=1,args = (new_rows,target_column,separator))
new_df = pd.DataFrame(new_rows)
return new_df