147 lines
5.7 KiB
Python
147 lines
5.7 KiB
Python
from typing import List, Union
|
||
|
||
import numpy as np
|
||
|
||
|
||
# 解决百分比相加不为100%
|
||
def fix_percentages(values: List[float], total: float) -> List[float]:
|
||
"""
|
||
修正百分比相加不为100%的问题。
|
||
|
||
Args:
|
||
values (list[float]): 百分比列表,元素个数与总和相同
|
||
total (float): 总和
|
||
|
||
Returns:
|
||
list[float]: 修正后的百分比列表
|
||
|
||
Examples:
|
||
>>> values = [0.2, 0.3, 0.5]
|
||
>>> total = 1
|
||
>>> fix_percentages(values, total)
|
||
[20.0, 30.0, 50.0]
|
||
|
||
>>> values = [0.2, 0.3, 0.5]
|
||
>>> total = 0.8
|
||
>>> fix_percentages(values, total)
|
||
[25.0, 37.5, 62.5]
|
||
"""
|
||
exact = [v / total * 100 for v in values]
|
||
floor = [np.floor(p * 100) / 100 for p in exact] # 向下取整到2位小数
|
||
remainders = [exact[i] - floor[i] for i in range(len(exact))]
|
||
|
||
# 需要分配的百分点数(以0.01%为单位)
|
||
to_distribute = int(round(10000 - sum(floor) * 100))
|
||
|
||
# 按余数大小分配
|
||
indices = sorted(range(len(remainders)), key=lambda i: remainders[i], reverse=True)
|
||
|
||
fixed = floor.copy()
|
||
for i in range(to_distribute):
|
||
fixed[indices[i]] += 0.01
|
||
|
||
return [round(p, 2) for p in fixed]
|
||
|
||
|
||
# === 误差矫正 ===
|
||
def correct_rounding_error(target_total:Union[int,float], adjusted_areas:List[float], original_areas:List[float]) -> List[int]:
|
||
"""
|
||
健壮的数值舍入误差矫正函数:将浮点型面积值四舍五入后,调整至目标总和。
|
||
核心逻辑:基于原始数值的小数部分优先级,逐次增减1来抵消舍入误差,确保最终总和匹配目标值,
|
||
同时避免调整后数值出现负数,防止无限循环。
|
||
|
||
Args:
|
||
target_total (int/float): 目标总和(最终舍入后数值的合计值),函数内部会转为整型
|
||
adjusted_areas (list[float]): 经过比例调整后的浮点型面积列表(待舍入的原始数据)
|
||
original_areas (list[float]): 调整前的原始浮点型面积列表(用于计算小数部分优先级)
|
||
|
||
Returns:
|
||
list[int]: 矫正后的整型面积列表,总和尽可能接近/等于target_total;
|
||
若无法完全矫正,返回尽可能接近的结果并打印警告
|
||
|
||
Raises:
|
||
无显式抛出异常,所有异常会被捕获并打印错误信息,返回保底的四舍五入结果
|
||
|
||
Notes:
|
||
1. 误差矫正规则:
|
||
- 误差>0(当前总和 < 目标总和):优先给小数部分大的数值+1
|
||
- 误差<0(当前总和 > 目标总和):优先给小数部分小的数值-1
|
||
2. 边界限制:调整时确保数值≥0(避免出现负数面积)
|
||
3. 防无限循环:最大迭代次数为 len(adjusted_areas) * 10,超出则终止并提示剩余误差
|
||
|
||
Examples:
|
||
>>> target = 10
|
||
>>> adjusted = [3.2, 2.8, 4.1] # 四舍五入后总和=3+3+4=10,无误差
|
||
>>> original = [3.2, 2.8, 4.1]
|
||
>>> correct_rounding_error(target, adjusted, original)
|
||
[3, 3, 4]
|
||
|
||
>>> target = 10
|
||
>>> adjusted = [3.1, 2.1, 4.1] # 四舍五入后总和=3+2+4=9,误差+1
|
||
>>> original = [3.1, 2.1, 4.1]
|
||
>>> correct_rounding_error(target, adjusted, original)
|
||
[3, 2, 5] # 优先给小数部分最大的4.1+1
|
||
|
||
>>> target = 8
|
||
>>> adjusted = [3.9, 2.9, 1.9] # 四舍五入后总和=4+3+2=9,误差-1
|
||
>>> original = [3.9, 2.9, 1.9]
|
||
>>> correct_rounding_error(target, adjusted, original)
|
||
[3, 3, 2] # 优先给小数部分最小的1.9-1(实际小数1.9>2.9>3.9,故调整3.9)
|
||
"""
|
||
try:
|
||
target_total = int(target_total)
|
||
rounded_areas = [int(round(area)) for area in adjusted_areas]
|
||
current_total = sum(rounded_areas)
|
||
error = target_total - current_total
|
||
|
||
if error == 0 or len(adjusted_areas) == 0:
|
||
return rounded_areas
|
||
|
||
# 使用循环分配直到误差为0或无法再分配
|
||
remaining_error = error
|
||
max_iterations = len(adjusted_areas) * 10 # 防止无限循环
|
||
|
||
for _ in range(max_iterations):
|
||
if remaining_error == 0:
|
||
break
|
||
|
||
# 每次迭代重新计算小数部分和排序
|
||
decimal_parts = [float(area - int(area)) for area in original_areas]
|
||
indices = list(range(len(adjusted_areas)))
|
||
|
||
if remaining_error > 0:
|
||
indices.sort(key=lambda i: decimal_parts[i], reverse=True)
|
||
adjustment = 1
|
||
else:
|
||
indices.sort(key=lambda i: decimal_parts[i])
|
||
adjustment = -1
|
||
|
||
# 尝试分配一次调整
|
||
adjusted = False
|
||
for idx in indices:
|
||
if (adjustment == 1 and rounded_areas[idx] >= 0) or (adjustment == -1 and rounded_areas[idx] > 0):
|
||
rounded_areas[idx] += adjustment
|
||
remaining_error -= adjustment
|
||
adjusted = True
|
||
break
|
||
|
||
if not adjusted: # 无法再调整
|
||
break
|
||
|
||
if remaining_error != 0:
|
||
print(f"警告:无法完全矫正误差,剩余: {remaining_error}")
|
||
|
||
return rounded_areas
|
||
|
||
except Exception as e:
|
||
print(f"误差矫正出错: {e}")
|
||
# 返回原始四舍五入结果作为保底
|
||
return [int(round(area)) for area in adjusted_areas]
|
||
|
||
if __name__ == '__main__':
|
||
target = 10
|
||
adjusted = [3.3, 3.9, 4.2] # 四舍五入后总和=3+3+4=10,无误差
|
||
original = [3.25, 2.85, 4.15]
|
||
print(correct_rounding_error(target, adjusted, original))
|
||
|
||
|