根据多级数据框中列中的数据存在来排列数据框

Arrage dataframe based on the data presence in columns in multilevel dataframe

我在 pandas df 中有一个多级列,索引为 appid 如下:

year   |2016    2017    2018    2019    2016  2017   2018   2019
       |ttl     ttl     ttl     ttl     tta   tta    tta    tta
-----------------------------------------------------------------
appid  |
75787  |NaN     227.0   470.0   426.0   NaN   25.0   23.0   21.0
146306 |NaN     858.0   226.0   NaN     NaN   14.0   35.0   NaN
159479 |NaN     NaN     0.0     NaN     NaN   NaN    3.5    NaN
163618 |NaN     0.0     650.0   100.0   NaN   12.0   14.6   123.0
215968 |23.0    0.0     NaN     NaN     45.0  2.0    NaN    NaN

我想将此 df 转换为可以通过存在的最新年份条目进行排序的方式。例如。

Year   |P2Y      PY      LY    P2Y    PY    LY
       |ttl     ttl     ttl    tta    tta   tta

----------------------------------------------------
appid  |
75787  |227.0   470.0   426.0  25.0   23.0   21.0
146306 |NaN     858.0   226.0  NaN    14.0   35.0
159479 |NaN     NaN     0.0    NaN    NaN    3.5
163618 |0.0     650.0   100.0  12.0   14.6   123.0
215968 |NaN     23.0    0.0    NaN    45.0   2.0

您可以尝试处理转置数据集并使用 shift:

df.T \
  .apply(lambda x: x.shift(len(x) - x.index.get_loc(x.last_valid_index()) - 1)) \
  .T \
  .dropna(how='all', axis='columns'))

说明

  1. 使用 .T

  2. 转置数据集
  3. 将每列移动指定数量的 NaN 列末尾的值

    1. 在每列上使用 apply

    2. 使用 last_valid_index with get_loc 查找最后一个不是 NaN 的值。有关此步骤的更多详细信息,请参阅 Locate first and last non NaN values in a Pandas DataFrame

    3. 计算步骤 2.3 和 len(x) 的行移位数。还要减去 1,因为步骤 2.2 中的索引采用上面的行索引。

    4. 使用shift移动列

  4. 最终使用 .T

  5. 将数据集转回第 1 步
  6. 使用 dropnahow='all', axis='columns'

  7. 删除所有 NaN

代码+插图

# Step 1
print(df.T)
#             75787   146306  159479  163618  215968
# year appid
# 2016 ttl       NaN     NaN     NaN     NaN    23.0
# 2017 ttl     227.0   858.0     NaN     0.0     0.0
# 2018 ttl     470.0   226.0     0.0   650.0     NaN
# 2019 ttl     426.0     NaN     NaN   100.0     NaN
# 2016 tta       NaN     NaN     NaN     NaN    45.0
# 2017 tta      25.0    14.0     NaN    12.0     2.0
# 2018 tta      23.0    35.0     3.5    14.6     NaN
# 2019 tta      21.0     NaN     NaN   123.0     NaN


# Step 2.2.1
print(df.T.apply(lambda x: x.last_valid_index()))
# 75787     (2019, tta)
# 146306    (2018, tta)
# 159479    (2018, tta)
# 163618    (2019, tta)
# 215968    (2017, tta)
# dtype: object


# Step 2.2.2
print(df.T.apply(lambda x: x.index.get_loc(x.last_valid_index())))
# 75787     7
# 146306    6
# 159479    6
# 163618    7
# 215968    5
# dtype: int64


# Step 2
print(df.T.apply(lambda x: x.shift(
    len(x) - x.index.get_loc(x.last_valid_index()) - 1)))
#             75787   146306  159479  163618  215968
# year appid
# 2016 ttl       NaN     NaN     NaN     NaN     NaN
# 2017 ttl     227.0     NaN     NaN     0.0     NaN
# 2018 ttl     470.0   858.0     NaN   650.0    23.0
# 2019 ttl     426.0   226.0     0.0   100.0     0.0
# 2016 tta       NaN     NaN     NaN     NaN     NaN
# 2017 tta      25.0     NaN     NaN    12.0     NaN
# 2018 tta      23.0    14.0     NaN    14.6    45.0
# 2019 tta      21.0    35.0     3.5   123.0     2.0


# Step 3
print(df.T.apply(lambda x: x.shift(
    len(x) - x.index.get_loc(x.last_valid_index()) - 1)).T)
# year   2016   2017   2018   2019 2016  2017  2018   2019
# appid   ttl    ttl    ttl    ttl  tta   tta   tta    tta
# 75787   NaN  227.0  470.0  426.0  NaN  25.0  23.0   21.0
# 146306  NaN    NaN  858.0  226.0  NaN   NaN  14.0   35.0
# 159479  NaN    NaN    NaN    0.0  NaN   NaN   NaN    3.5
# 163618  NaN    0.0  650.0  100.0  NaN  12.0  14.6  123.0
# 215968  NaN    NaN   23.0    0.0  NaN   NaN  45.0    2.0



# Step 4
print(df.T.apply(lambda x: x.shift(
    len(x) - x.index.get_loc(x.last_valid_index()) - 1)).T
    .dropna(how='all', axis='columns'))

# year     2017   2018   2019  2017  2018   2019
# appid     ttl    ttl    ttl   tta   tta    tta
# 75787   227.0  470.0  426.0  25.0  23.0   21.0
# 146306    NaN  858.0  226.0   NaN  14.0   35.0
# 159479    NaN    NaN    0.0   NaN   NaN    3.5
# 163618    0.0  650.0  100.0  12.0  14.6  123.0
# 215968    NaN   23.0    0.0   NaN  45.0    2.0

如有必要,您可以先DataFrame.stack for years to columns, then use justify, filter last 3 columns, create DataFrame and reshape back by DataFrame.unstack with DataFrame.reindex更改列名称的顺序:

df1 = df.stack()

arr = justify(df1.to_numpy(),invalid_val=np.nan, side='right')[:, -3:]
print (arr)
[[ 25.   23.   21. ]
 [227.  470.  426. ]
 [  nan  14.   35. ]
 [  nan 858.  226. ]
 [  nan   nan   3.5]
 [  nan   nan   0. ]
 [ 12.   14.6 123. ]
 [  0.  650.  100. ]
 [  nan  45.    2. ]
 [  nan  23.    0. ]]


mux = pd.MultiIndex.from_product([df.columns.levels[1], ['P2Y','PY','LY']])
df2 = (pd.DataFrame(arr, index=df1.index, columns=['P2Y','PY','LY'])
         .unstack()
         .swaplevel(1,0, axis=1)
         .reindex(mux, axis=1))
print (df2)
         tta                 ttl              
         P2Y    PY     LY    P2Y     PY     LY
75787   25.0  23.0   21.0  227.0  470.0  426.0
146306   NaN  14.0   35.0    NaN  858.0  226.0
159479   NaN   NaN    3.5    NaN    NaN    0.0
163618  12.0  14.6  123.0    0.0  650.0  100.0
215968   NaN  45.0    2.0    NaN   23.0    0.0

函数:

#
def justify(a, invalid_val=0, axis=1, side='left'):    
    """
    Justifies a 2D array

    Parameters
    ----------
    A : ndarray
        Input array to be justified
    axis : int
        Axis along which justification is to be made
    side : str
        Direction of justification. It could be 'left', 'right', 'up', 'down'
        It should be 'left' or 'right' for axis=1 and 'up' or 'down' for axis=0.

    """

    if invalid_val is np.nan:
        mask = ~np.isnan(a)
    else:
        mask = a!=invalid_val
    justified_mask = np.sort(mask,axis=axis)
    if (side=='up') | (side=='left'):
        justified_mask = np.flip(justified_mask,axis=axis)
    out = np.full(a.shape, invalid_val) 
    if axis==1:
        out[justified_mask] = a[mask]
    else:
        out.T[justified_mask.T] = a.T[mask.T]
    return out