【问题标题】:Split (explode) range in dataframe into multiple rows将数据框中的范围拆分(分解)为多行
【发布时间】:2018-02-13 21:49:43
【问题描述】:

这个问题类似于Split (explode) pandas dataframe string entry to separate rows,但包含一个关于添加范围的问题。

我有一个数据框:

+------+---------+----------------+
| Name | Options | Email          |
+------+---------+----------------+
| Bob  | 1,2,4-6 | bob@email.com  |
+------+---------+----------------+
| John |   NaN   | john@email.com |
+------+---------+----------------+
| Mary |   1,2   | mary@email.com |
+------+---------+----------------+
| Jane | 1,3-5   | jane@email.com |
+------+---------+----------------+

我希望Options 列被逗号分隔,以及为范围添加的行。

+------+---------+----------------+
| Name | Options | Email          |
+------+---------+----------------+
| Bob  | 1       | bob@email.com  |
+------+---------+----------------+
| Bob  | 2       | bob@email.com  |
+------+---------+----------------+
| Bob  | 4       | bob@email.com  |
+------+---------+----------------+
| Bob  | 5       | bob@email.com  |
+------+---------+----------------+
| Bob  | 6       | bob@email.com  |
+------+---------+----------------+
| John | NaN     | john@email.com |
+------+---------+----------------+
| Mary | 1       | mary@email.com |
+------+---------+----------------+
| Mary | 2       | mary@email.com |
+------+---------+----------------+
| Jane | 1       | jane@email.com |
+------+---------+----------------+
| Jane | 3       | jane@email.com |
+------+---------+----------------+
| Jane | 4       | jane@email.com |
+------+---------+----------------+
| Jane | 5       | jane@email.com |
+------+---------+----------------+

我如何才能像参考 SO 文章所说的那样使用 concatsplit 来实现这一点?我需要一种添加范围的方法。

那篇文章使用以下代码来拆分逗号分隔的值 (1,2,3):

In [7]: a
Out[7]: 
    var1  var2
0  a,b,c     1
1  d,e,f     2

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

提前感谢您的建议!

更新 2/14 示例数据已更新以匹配我当前的情况。

【问题讨论】:

标签: python pandas numpy dataframe


【解决方案1】:

如果我明白你需要什么

def yourfunc(s):
    ranges = (x.split("-") for x in s.split(","))

    return [i for r in ranges for i in range(int(r[0]), int(r[-1]) + 1)]


df.Options=df.Options.apply(yourfunc)

df
Out[114]: 
   Name          Options           Email
0   Bob  [1, 2, 4, 5, 6]   bob@email.com
1  Jane     [1, 3, 4, 5]  jane@email.com


df.set_index(['Name','Email']).Options.apply(pd.Series).stack().reset_index().drop('level_2',1)
Out[116]: 
   Name           Email    0
0   Bob   bob@email.com  1.0
1   Bob   bob@email.com  2.0
2   Bob   bob@email.com  4.0
3   Bob   bob@email.com  5.0
4   Bob   bob@email.com  6.0
5  Jane  jane@email.com  1.0
6  Jane  jane@email.com  3.0
7  Jane  jane@email.com  4.0
8  Jane  jane@email.com  5.0

【讨论】:

  • 问个问题,为什么Option栏会变成float?
  • @jp_data_analysis 应用 pd.Series 时,有 np.nan ,然后在 stack(even np.nan has been removed as defualt) 之后,它变成 float :-)
【解决方案2】:

我喜欢使用np.r_slice
我知道这看起来一团糟,但情人眼里却是美丽。

def parse(o):
    mm = lambda i: slice(min(i), max(i) + 1)
    return np.r_.__getitem__(tuple(
        mm(list(map(int, s.strip().split('-')))) for s in o.split(',')
    ))

r = df.Options.apply(parse)
new = np.concatenate(r.values)
lens = r.str.len()

df.loc[df.index.repeat(lens)].assign(Options=new)

   Name  Options           Email
0   Bob        1   bob@email.com
0   Bob        2   bob@email.com
0   Bob        4   bob@email.com
0   Bob        5   bob@email.com
0   Bob        6   bob@email.com
2  Mary        1  mary@email.com
2  Mary        2  mary@email.com
3  Jane        1  jane@email.com
3  Jane        3  jane@email.com
3  Jane        4  jane@email.com
3  Jane        5  jane@email.com

说明

  • np.r_ 采用不同的切片器和索引器并返回组合的数组。

    np.r_[1, 4:7]
    array([1, 4, 5, 6])
    

    np.r_[slice(1, 2), slice(4, 7)]
    array([1, 4, 5, 6])
    

    但如果我需要传递任意一堆,我需要将tuple 传递给np.r_ s __getitem__ 方法。

    np.r_.__getitem__((slice(1, 2), slice(4, 7), slice(10, 14)))
    array([ 1,  4,  5,  6, 10, 11, 12, 13])
    

    所以我迭代、解析、制作切片并传递给np.r_.__getitem__

  • 在应用我很酷的解析器后,使用 locpd.Index.repeatpd.Series.str.len 的组合

  • 使用pd.DataFrame.assign 覆盖现有列

__注意__
如果您的 Options 列中有错误字符,我会尝试像这样过滤。

df = df.dropna(subset=['Options']).astype(dict(Options=str)) \
       .replace(dict(Options={'[^0-9,\-]': ''}), regex=True) \
       .query('Options != ""')

【讨论】:

  • 有趣!感谢您解释解决方案。当调用 mm() 时,我得到一个 AttributeError,其中“'float' 对象没有属性 'split'”。知道为什么会这样吗?
  • 我猜这可能是由于我的列中的空值。
  • 这绝对是原因。第一滴
  • 所以我这样做了 (df_2 = df[df['Options'].notnull()])) 并且我发现自己遇到了一个 ValueError (ValueError: invalid literal for int() with base 10: '') for r = df_2.Options... - 我认为这可能是因为您不能将字符串转换为浮点数?还是我弄错了?
  • 这意味着选项字符串中还有其他字符,您没有在示例数据中表示。尝试int('^') 重现错误。如果您粘贴了整个错误,那么您必须在其中包含非打印字符。您可以先尝试剥离它们。
【解决方案3】:

从自定义替换函数开始:

def replace(x):
    i, j = map(int, x.groups())
    return ','.join(map(str, range(i, j + 1)))

将列名存储在某处,稍后我们将使用它们:

c = df.columns

接下来,替换df.Options中的项目,然后用逗号分割:

v = df.Options.str.replace('(\d+)-(\d+)', replace).str.split(',')

接下来,重塑你的数据并最终加载到一个新的数据帧中:

df = pd.DataFrame(
       df.drop('Options', 1).values.repeat(v.str.len(), axis=0)
)
df.insert(c.get_loc('Options'), len(c) - 1, np.concatenate(v))
df.columns = c

df

   Name Options           Email
0   Bob       1   bob@email.com
1   Bob       2   bob@email.com
2   Bob       4   bob@email.com
3   Bob       5   bob@email.com
4   Bob       6   bob@email.com
5  Jane       1  jane@email.com
6  Jane       3  jane@email.com
7  Jane       4  jane@email.com
8  Jane       5  jane@email.com

【讨论】:

  • 首先,感谢您提供的超有创意的解决方案!我在 df.drop 行上收到 TypeError。完整错误是:“无法根据规则‘安全’将数组数据从 dtype('float64') 转换为 dtype('int64')”
  • @kabaname 似乎 df.Options 有 NaN。我目前不在键盘前,但当我在时,我可以尝试为您提供处理此问题的解决方案。你可以只删除行吗?这将简化解决方案。
  • 啊不幸的是,我认为我不能删除这些行,因为空单元格在一行中的其他列中仍然有我想保留的信息。抱歉,我应该在 OP 中指出这一点。你认为我可以创建一个新的 notnull() DF,执行操作并将其添加回来吗?
  • 好的,使用来自@piRSquared 的 NOTE 我能够创建 df。但是,当我到达 df.insert 时,我得到一个键错误:0。那是因为所有 NaN 行都被删除了吗?例如,如果我有一个索引为 [0,1,2,3] 的 DF,并且第 0,1 行被删除,因为它们的 Options 列是 NaN,这是否意味着当我尝试执行 df.insert (把 df 放回去替换原始值)它会失败,因为技术上没有 0?
【解决方案4】:

这是一种解决方案。虽然它不是很漂亮(pandas 的使用最少),但它相当有效。

import itertools, pandas as pd, numpy as np; concat = itertools.chain.from_iterable

def ranger(mystr):
    return list(concat([int(i)] if '-' not in i else \
                list(range(int(i.split('-')[0]), int(i.split('-')[-1])+1)) \
                for i in mystr.split(',')))

df = pd.DataFrame([['Bob', '1,2,4-6', 'bob@email.com'],
                   ['Jane', '1,3-5', 'jane@email.com']],
                  columns=['Name', 'Options', 'Email'])

df['Options'] = df['Options'].map(ranger)

lens = list(map(len, df['Options']))

df_out = pd.DataFrame({'Name': np.repeat(df['Name'].values, lens),
                       'Email': np.repeat(df['Email'].values, lens),
                       'Option': np.hstack(df['Options'].values)})

#             Email  Name  Option
# 0   bob@email.com   Bob       1
# 1   bob@email.com   Bob       2
# 2   bob@email.com   Bob       4
# 3   bob@email.com   Bob       5
# 4   bob@email.com   Bob       6
# 5  jane@email.com  Jane       1
# 6  jane@email.com  Jane       3
# 7  jane@email.com  Jane       4
# 8  jane@email.com  Jane       5

基准测试以下 4 个解决方案(仅供参考)。

一般来说,repeat 品种的表现优于其他品种。此外,从头开始创建新数据帧的解决方案(而不是apply)做得更好。下拉至numpy 可获得最佳结果。

import itertools, pandas as pd, numpy as np; concat = itertools.chain.from_iterable

def ranger(mystr):
    return list(concat([int(i)] if '-' not in i else \
                list(range(int(i.split('-')[0]), int(i.split('-')[-1])+1)) \
                for i in mystr.split(',')))

def replace(x):
    i, j = map(int, x.groups())
    return ','.join(map(str, range(i, j + 1)))

def yourfunc(s):
    ranges = (x.split("-") for x in s.split(","))
    return [i for r in ranges for i in range(int(r[0]), int(r[-1]) + 1)]

def parse(o):
    mm = lambda i: slice(min(i), max(i) + 1)
    return np.r_.__getitem__(tuple(mm(list(map(int, s.strip().split('-')))) for s in o.split(',')))

df = pd.DataFrame([['Bob', '1,2,4-6', 'bob@email.com'],
                   ['Jane', '1,3-5', 'jane@email.com']],
                  columns=['Name', 'Options', 'Email'])

df = pd.concat([df]*1000, ignore_index=True)

def explode_jp(df):
    df['Options'] = df['Options'].map(ranger)
    lens = list(map(len, df['Options']))
    df_out = pd.DataFrame({'Name': np.repeat(df['Name'].values, lens),
                           'Email': np.repeat(df['Email'].values, lens),
                           'Option': np.hstack(df['Options'].values)})
    return df_out

def explode_cs(df):
    c = df.columns
    v = df.Options.str.replace('(\d+)-(\d+)', replace).str.split(',')
    df_out = pd.DataFrame(df.drop('Options', 1).values.repeat(v.str.len(), axis=0))
    df_out.insert(c.get_loc('Options'), len(c) - 1, np.concatenate(v))
    df_out.columns = c
    return df_out

def explode_wen(df):
    df.Options=df.Options.apply(yourfunc)
    df_out = df.set_index(['Name','Email']).Options.apply(pd.Series).stack().reset_index().drop('level_2',1)
    return df_out

def explode_pir(df):
    r = df.Options.apply(parse)
    df_out = df.loc[df.index.repeat(r.str.len())].assign(Options=np.concatenate(r))
    return df_out

%timeit explode_jp(df.copy())   # 32.7 ms ± 1.54 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit explode_cs(df.copy())   # 90.6 ms ± 2.07 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit explode_wen(df.copy())  # 675 ms ± 12.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit explode_pir(df.copy())  # 163 ms ± 1.97 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

【讨论】:

  • 您的时间安排不公平,因为您的答案适合这种特定情况,但对于任何其他数量的列都将失败。像我所做的那样进行概括并不容易,而且肯定会损害性能。 :)
  • @COLDSPEED,你是对的。这就是为什么我赞成你的回答。但是我做这些时间是为了我自己的教育,你仍然可以从 4 个非常不同的答案的表现中学到很多东西。
猜你喜欢
  • 1970-01-01
  • 2016-06-06
  • 1970-01-01
  • 2020-02-28
  • 2013-11-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多