You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Python中按行调整DataFrame资产占比至符合限额

投资组合限额调整:R代码转Python实现

需求说明

已通过Python完成投资组合优化,得到资产占比结果DataFrame(每行数值和为1),另有各资产的限额DataFrame。需实现以下逻辑:

  • 将超出限额的资产占比分配给未达限额的资产
  • 完成上限调整后,检查资产占比下限,将低于下限的部分分配给仍有空间的资产
    现有实现该逻辑的R代码,需转为Python实现。

输入数据

资产占比DataFrame

import pandas as pd

Column_names = ['Asset_1', 'Asset_2', 'Asset_3', 'Asset_4', 'Asset_5', 'Asset_6',
                'Asset_7', 'Asset_8', 'Asset_9', 'Asset_10']

List_values = [[0.1 , 0.  , 0.  , 0.  , 0.3 , 0.01, 0.01, 0.01, 0.02, 0.55],
               [0.1 , 0.  , 0.  , 0.02, 0.1 , 0.01, 0.02, 0.73, 0.02, 0.  ],
               [0.1 , 0.  , 0.  , 0.03, 0.1 , 0.02, 0.25, 0.  , 0.25, 0.25],
               [0.1 , 0.  , 0.  , 0.04, 0.  , 0.01, 0.  , 0.85, 0.  , 0.  ],
               [0.1 , 0.  , 0.  , 0.05, 0.  , 0.01, 0.  , 0.84, 0.  , 0.  ],
               [0.1 , 0.  , 0.  , 0.06, 0.  , 0.01, 0.6 , 0.  , 0.  , 0.23],
               [0.  , 0.  , 0.  , 0.07, 0.  , 0.01, 0.6 , 0.  , 0.32, 0.  ],
               [0.  , 0.  , 0.  , 0.08, 0.  , 0.01, 0.  , 0.  , 0.6 , 0.31],
               [0.  , 0.  , 0.  , 0.09, 0.  , 0.01, 0.  , 0.6 , 0.  , 0.3 ]]

portfolio_df = pd.DataFrame(data=List_values, columns=Column_names)

资产限额DataFrame

limits_df = pd.DataFrame({
    'Asset_1': [0.2], 'Asset_2': [0.2], 'Asset_3': [0.2], 'Asset_4': [0.2],
    'Asset_5': [0.2], 'Asset_6': [0.1], 'Asset_7': [0.3], 'Asset_8': [0.45],
    'Asset_9': [0.15], 'Asset_10': [0.3]
})
# 转为Series方便处理
limits = limits_df.iloc[0]

期望输出

Desired_List_values = [
    [0.08, 0.  , 0.  , 0.  , 0.  , 0.45, 0.  , 0.  , 0.3 , 0.15],
    [0.  , 0.  , 0.  , 0.07, 0.  , 0.45, 0.  , 0.  , 0.3 , 0.15],
    [0.  , 0.  , 0.  , 0.07, 0.  , 0.45, 0.  , 0.  , 0.3 , 0.15],
    [0.  , 0.  , 0.  , 0.07, 0.  , 0.45, 0.  , 0.  , 0.3 , 0.15],
    [0.  , 0.  , 0.  , 0.07, 0.  , 0.45, 0.  , 0.  , 0.3 , 0.15],
    [0.  , 0.  , 0.  , 0.09, 0.  , 0.44, 0.  , 0.  , 0.3 , 0.15],
    [0.  , 0.  , 0.  , 0.1 , 0.  , 0.43, 0.  , 0.  , 0.3 , 0.15]
]

desired_df = pd.DataFrame(data=Desired_List_values, columns=Column_names)

注:原期望输出中的第一列序号为额外添加,此处已移除,仅保留资产占比数据。

原R代码

mpShareDistrib <- function(x, l = 1, m = 1, dtz = F){

  # 0. Check inputs
  ss <- as.numeric(x)
  names(ss) <- names(x)
  ss <- na.omit(ss)

  l <- as.numeric(l)

  if(length(l) == 1) l <- rep(l, times = length(ss))
  if(length(l) != length(ss)) stop('Shares and limit vectors are not equal')

  m <- as.numeric(m)

  if(is.null(names(ss))) names(ss) <- paste0('sec', 1:length(ss))
  if(is.null(names(l))) names(l) <- names(ss)
  if(length(setdiff(names(ss), names(l))) > 0)
    stop('Shares and limit have different names for:', setdiff(names(ss), names(l)) )

  Cut_Share <- function(x, l, m){

    ss1 <- x

    ss1 <- ss1[order(names(ss1))]
    l1  <- l[names(ss1)]

    i <- 1
    while(max(ss1-l1)>0 & i < 10){

      # (above limit stock shares)
      abv_ss <- ss1[ss1>0] - l1[ss1>0]
      abv_ss <- abv_ss[abv_ss > 0]
      abv_ss <- sum(abv_ss)

      new1_ss <- ss1[which((ss1 - l1)>0)]
      new1_l  <- l[names(new1_ss)]
      new1_ss <- new1_ss - (new1_ss - new1_l)

      # (free stock stares)
      free_ss <- ss1[ss1 < l1]

      # (filled stock shares)
      fld_ss <- free_ss + abv_ss * free_ss / sum(free_ss)

 
      new1_ss <- c(new1_ss, ss1[ss1 == l1], fld_ss)

      ss1 <- new1_ss
      ss1 <- round(ss1, digits = 4)
      ss1 <- ss1[order(names(ss1))]

      i <- i + 1
    }

    return(ss1)
  }

  ss1 <- Cut_Share(ss[ss>0], l, m)

  ss2 <- ss1[ss1 > 0]  
  l2  <- l[names(ss2)]

  ss2 <- ss2[order(names(ss2))]
  l2  <- l2[order(names(l2))]

  i <- 1

  while(min(ss2) < m & i < 10){

    min_ss <- ss2[which.min(ss2)]

    free_ss2 <- ss2[-which.min(ss2)]
    free_l2 <- l2[names(free_ss2)]

    free_ss2 <- free_ss2[free_ss2 < free_l2]

    if(length(free_ss2) > 0)
    {

      free_l2 <- free_l2[names(free_ss2)]

      dstr <- free_ss2 / sum(free_ss2) * min_ss

      fld_ss  <- pmin(free_ss2 + dstr, free_l2)

      rem <- min_ss - sum(fld_ss - free_ss2)
      rem <- round(rem, digits = 4)
      names(rem) <- names(min_ss)

      if(rem > 0)
        new2_ss <- c(ss2[ss2 >= l2], fld_ss, rem)
      else
        new2_ss <- c(ss2[ss2 >= l2], fld_ss)

    } else {

      warning('Cannot distribute small share - no free space ')
      break()
    }

    ss2 <- new2_ss
    l2  <- l2[names(new2_ss)]
    i <- i + 1

  }

  # 3. Cut shares above limit
  #
  ss3 <- ss2

  # 4. Finalization
  #
  # Add distributed share with zero value
  {
    lngth_diff <- length(ss) - length(ss3)

    ss0 <- c()
    if(lngth_diff > 0){

      ss0 <- rep(0, lngth_diff)

      rmvd_ss <- setdiff(names(ss), names(ss3))
      names(ss0) <- rmvd_ss
    }

    ss_fin <- c(ss0, ss3)
    ss_fin <- ss_fin[order(names(ss_fin))]

    if(is.null(names(ss))) names(ss_fin) <- NULL

  }

  if(sum(ss) != sum(ss_fin)) warning('Shares distribution error')

  return(ss_fin)

}

Python实现代码

import pandas as pd
import numpy as np

def mp_share_distrib(x, limits, min_limit=1):
    # 输入处理:转为Series,保留非空值
    ss = pd.Series(x).dropna()
    ss = ss[ss > 0]  # 仅保留正占比资产
    limits = pd.Series(limits)
    
    # 检查输入长度匹配
    if len(limits) == 1:
        limits = pd.Series([limits.iloc[0]] * len(ss), index=ss.index)
    if len(ss) != len(limits):
        raise ValueError("Shares and limit vectors are not equal")
    
    # 确保索引一致
    if not ss.index.equals(limits.index):
        missing = ss.index.difference(limits.index)
        raise ValueError(f"Shares and limit have different names for: {list(missing)}")
    
    def cut_share(ss_input, limits_input):
        ss1 = ss_input.copy().sort_index()
        l1 = limits_input.loc[ss1.index].sort_index()
        
        i = 1
        while (ss1 - l1).max() > 0 and i < 10:
            # 计算超出限额的总金额
            abv_mask = (ss1 > 0) & (ss1 > l1)
            abv_ss = (ss1[abv_mask] - l1[abv_mask]).sum()
            
            # 超出部分截断到限额
            new1_ss = ss1[abv_mask].copy()
            new1_ss = l1[abv_mask]
            
            # 未达限额的资产
            free_mask = ss1 < l1
            free_ss = ss1[free_mask]
            
            # 分配超出部分:按当前占比权重分配
            if free_ss.sum() > 0:
                fld_ss = free_ss + abv_ss * (free_ss / free_ss.sum())
            else:
                fld_ss = free_ss
            
            # 合并所有资产:截断后的、刚好达限额的、分配后的
            exact_mask = ss1 == l1
            new1_ss = pd.concat([new1_ss, ss1[exact_mask], fld_ss]).sort_index()
            ss1 = new1_ss.round(4)
            
            i += 1
        return ss1
    
    # 第一步:处理上限超额分配
    ss1 = cut_share(ss, limits)
    
    # 第二步:处理下限不足分配
    ss2 = ss1[ss1 > 0].sort_index()
    l2 = limits.loc[ss2.index].sort_index()
    
    i = 1
    while ss2.min() < min_limit and i < 10:
        min_idx = ss2.idxmin()
        min_val = ss2[min_idx]
        
        # 排除当前最小的资产,找仍有空间的资产
        free_ss2 = ss2.drop(min_idx)
        free_l2 = l2.drop(min_idx)
        free_mask = free_ss2 < free_l2
        free_ss2 = free_ss2[free_mask]
        free_l2 = free_l2[free_mask]
        
        if len(free_ss2) == 0:
            print("Warning: Cannot distribute small share - no free space")
            break
        
        # 按当前占比权重分配最小部分
        total_free = free_ss2.sum()
        dstr = free_ss2 / total_free * min_val
        fld_ss = pd.Series(np.minimum(free_ss2 + dstr, free_l2), index=free_ss2.index)
        
        # 计算剩余未分配的部分
        rem = min_val - (fld_ss - free_ss2).sum()
        rem = round(rem, 4)
        
        # 合并结果:已达限额的、分配后的、剩余部分(如果有)
        exact_mask = ss2 >= l2
        new2_ss = pd.concat([ss2[exact_mask], fld_ss])
        if rem > 0:
            new2_ss = pd.concat([new2_ss, pd.Series([rem], index=[min_idx])])
        
        ss2 = new2_ss.sort_index()
        l2 = limits.loc[ss2.index].sort_index()
        i += 1
    
    # 第三步:补回零占比资产
    all_assets = pd.Series(0, index=x.index)
    all_assets.loc[ss2.index] = ss2
    ss_fin = all_assets.round(4)
    
    # 检查总和是否一致
    if not np.isclose(ss.sum(), ss_fin.sum(), atol=1e-4):
        print("Warning: Shares distribution error")
    
    return ss_fin

# 应用到整个DataFrame
def apply_portfolio_adjustment(portfolio_df, limits):
    adjusted_rows = []
    for idx, row in portfolio_df.iterrows():
        adjusted = mp_share_distrib(row, limits, min_limit=0)  # 此处min_limit根据需求调整,原R代码默认是1
        adjusted_rows.append(adjusted.values)
    return pd.DataFrame(adjusted_rows, columns=portfolio_df.columns)

# 执行调整
adjusted_portfolio = apply_portfolio_adjustment(portfolio_df, limits)
print(adjusted_portfolio)

说明

  • 原R代码中m参数为下限阈值,默认值为1,实际应用中需根据业务需求调整(例如如果下限是0,则传入min_limit=0)
  • 代码中保留了原R的迭代次数限制(最多10次),避免无限循环
  • 所有计算保留4位小数,与原R代码一致
  • 处理过程中严格保持资产索引一致,避免匹配错误

内容的提问来源于stack exchange,提问作者Olsen_I

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 03:45:53