【问题标题】:Filtering with MultiIndex使用 MultiIndex 过滤
【发布时间】:2018-08-20 14:47:33
【问题描述】:

我有一个像这样的 Pandas DataFrame:

import numpy as np
import pandas as pd

np.random.seed(1234)
midx = pd.MultiIndex.from_product([['a', 'b', 'c'], pd.date_range('20130101', periods=6)], names=['letter', 'date'])
df = pd.DataFrame(np.random.randn(len(midx), 1), index=midx)

该数据框如下所示:

                        0
letter      date    
  a     2013-01-01  0.471435
        2013-01-02  -1.190976
        2013-01-03  1.432707
        2013-01-04  -0.312652
        2013-01-05  -0.720589
        2013-01-06  0.887163
  b     2013-01-01  0.859588
        2013-01-02  -0.636524
        2013-01-03  0.015696
        2013-01-04  -2.242685
        2013-01-05  1.150036
        2013-01-06  0.991946
  c     2013-01-01  0.953324
        2013-01-02  -2.021255
        2013-01-03  -0.334077
        2013-01-04  0.002118
        2013-01-05  0.405453
        2013-01-06  0.289092

我想做的是根据 date 上的条件保留所有行,该条件取决于 letter。例如,

  • 对于字母a,我想保留所有行,以便日期在“20130102”和“20130105”(包括)之间
  • 对于字母 b,我想保留所有行,以便 date == "20130103"
  • 对于字母 c,我想保留所有行,以便 日期在“20130103”和“20130105”(包括)之间

例如,所有这些信息都可以存储在字典中。

dictionary = {"a": slice("20130102", "20130105"),
              "b": "20130103",
              "c": slice("20130103", "20130105")}

有没有一种简单的方法可以用 pandas 来计算这个?我没有找到有关此类过滤的任何信息。

【问题讨论】:

    标签: python pandas filtering multi-index


    【解决方案1】:

    你可以使用query,它就是为这种选择标准而设计的。

    如果您稍微修改您的dictionary,您可以在列表理解的帮助下生成您想要的查询:

    In : dictionary
    Out:
    {'a': ('20130102', '20130105'),
     'b': ('20130103', '20130103'),
     'c': ('20130103', '20130105')}
    
    In : df.query(
              ' or '.join("('{}' <= date <= '{}' and letter == '{}')".format(*(v + (k,))) 
              for k, v in dictionary.items())
             )
    Out:
                              0
    letter date
    a      2013-01-02 -1.190976
           2013-01-03  1.432707
           2013-01-04 -0.312652
           2013-01-05 -0.720589
    b      2013-01-03  0.015696
    c      2013-01-03 -0.334077
           2013-01-04  0.002118
           2013-01-05  0.405453
    

    有关查询语句实际执行的操作的更多信息,请参阅列表理解的详细信息:

    In : (' or '.join("('{}' <= date <= '{}' and letter == '{}')".format(*(v + (k,)))
              for k, v in dictionary.items()))
    Out: "('20130102' <= date <= '20130105' and letter == 'a') or 
              ('20130103' <= date <= '20130105' and letter == 'c') or
              ('20130103' <= date <= '20130103' and letter == 'b')"
    

    【讨论】:

      【解决方案2】:

      最简单的方法是将函数应用到 pandas DataFrameGroupBy 对象,这是一个示例:

      dictionary = {"a": slice("20130102", "20130105"),
                    "b": slice("20130103", "20130103"),
                    "c": slice("20130103", "20130105")}
      
      def date_condition(group, dictionary):
          return group.xs(group.name).loc[dictionary[group.name]]
      
      df.groupby(level=0).apply(date_condition, dictionary)
      
      Output[0]:
                                0
      letter date                
      a      2013-01-02 -1.190976
             2013-01-03  1.432707
             2013-01-04 -0.312652
             2013-01-05 -0.720589
      b      2013-01-03  0.015696
      c      2013-01-03 -0.334077
             2013-01-04  0.002118
             2013-01-05  0.405453
      

      注意“b”的切片重复日期以强制 .loc 返回 DataFrame 而不是 Series

      【讨论】:

        【解决方案3】:

        对原始字典稍作改动,我们可以更简洁地做到这一点。我们可以在列表理解中使用pd.IndexSlice,然后使用pd.concat

        # add `-` to separate dates
        dictionary = {"a": slice("2013-01-02", "2013-01-05"),
                      "b": "2013-01-03",
                      "c": slice("2013-01-03", "2013-01-05")}
        
        dictionary = OrderedDict(sorted(dictionary.items()))
        
        idx_slices = [pd.IndexSlice[k, v] for k, v in dictionary.items()]
        
        pd.concat([df.loc[idx, :] for idx in idx_slices])
        
        Out[1]:
                             0
        letter  date    
        a       2013-01-02   -1.190976
                2013-01-03   1.432707
                2013-01-04   -0.312652
                2013-01-05   -0.720589
        c       2013-01-03   -0.334077
                2013-01-04   0.002118
                2013-01-05   0.405453
        b       2013-01-03   0.015696
        

        如果您希望自动添加-,您可以使用datetime,如下所示,

        dt.datetime.strptime('20170121', '%Y%m%d').strftime('%Y-%m-%d')
        

        【讨论】:

        • 看起来不错,但实际上比我现在的要慢一点,因为单独的索引操作和连接
        • @BradSolomon - 我还没有计时,但我敢打赌你的时间会快一点。我认为我所拥有的更具可读性。不确定OP想要优化什么:)
        • 同意所有观点@josh。虽然是我打开赏金的人,眨眼眨眼
        【解决方案4】:

        这是一种笨拙的方式,但您可以使用以下事实:

        传递标签或元组列表的工作方式类似于重新索引 [source]

        并利用pd.Index.slice_indexer(start, stop),它允许您将每个索引过滤到指定日期之间。

        >>> dictionary = {"a": ("20130102", "20130105"),
        ...               "b": "20130103",
        ...               "c": ("20130103", "20130105")}
        ... 
        ... 
        ... def get_idx_pairs():
        ...     for lvl0, lvl1 in df.index.groupby(df.index.get_level_values(0)).items():
        ...         dates = lvl1.levels[1]
        ...         dt = dictionary[lvl0]
        ...         if isinstance(dt, (tuple, list)):
        ...             slices = dates[dates.slice_indexer(dt[0], dt[1])]
        ...             for s in slices:
        ...                 yield (lvl0, s)
        ...         else:
        ...             yield (lvl0, dt)
        ... 
        ... 
        ... df.loc[list(get_idx_pairs())]
        ... 
                                0
        letter date              
        a      2013-01-02 -1.1910
               2013-01-03  1.4327
               2013-01-04 -0.3127
               2013-01-05 -0.7206
        b      2013-01-03  0.0157
        c      2013-01-03 -0.3341
               2013-01-04  0.0021
               2013-01-05  0.4055
        

        对于 date 中的每个“较小”的 DatetimeIndex,您将其限制为指定的切片,然后构造 (letter, date) 的元组以显式索引。

        或者,如果您可以将日期指定为元组(对于单个日期,只需重复),您可以稍微压缩帮助函数:

        >>> dates = (("20130102", "20130105"),
        ...          ("20130103", "20130103"),
        ...          ("20130103", "20130105"))
        ... 
        ... def get_idx_pairs(df, dates):
        ...     letters = df.index.get_level_values(0)
        ...     for (k, v), (start, stop) in zip(df.index.groupby(letters).items(), dates):
        ...         dates = v.levels[1]
        ...         sliced = dates[dates.slice_indexer(start, stop)]
        ...         for s in sliced:
        ...             yield k, s
        ... 
        ... df.loc[list(get_idx_pairs(df, dates))]
        ... 
                                0
        letter date              
        a      2013-01-02 -1.1910
               2013-01-03  1.4327
               2013-01-04 -0.3127
               2013-01-05 -0.7206
        b      2013-01-03  0.0157
        c      2013-01-03 -0.3341
               2013-01-04  0.0021
               2013-01-05  0.4055
        

        【讨论】:

          猜你喜欢
          • 2022-12-07
          • 2017-08-25
          • 1970-01-01
          • 2017-03-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-12-04
          • 1970-01-01
          相关资源
          最近更新 更多