I am attempting to write a function in Python to find a list when entered, with a max value, will limit the values in the list and proportionally redistribute the excess subtracted from the number to the other numbers within the list.
For example, I want [0.05, 0.02, 0.05, 0.08, 0.80] passed through a function with a max of 0.3 to be [0.175, 0.07, 0.175, 0.28, 0.30].
Currently, I have something like this in Python:
'''
import numpy as np
x = np.array([0.05, 0.05, 0.02, 0.08, 0.80])
limit = 0.3
excess = 0
y = np.zeros(len(x))
z = np.zeros(len(x))
for i in range(len(x)):
if x[i] > limit:
excess = x[i] - limit
x[i] = 0
y[i] = limit
z = x*(1/sum(x)*excess)
z = z+x+y
'''
Where z is the result of this particular array.
'''
z = array([0.175, 0.175, 0.07 , 0.28 , 0.3 ])
'''
However, this breaks down when I have multiple numbers above the limit, or if the array is in a different order.
You can achieve your objective using the following script. The idea is to take the "excess weight" and redistribute it over the underweight elements repeatedly until all elements have been capped. The process needs to be repeated because redistribution may bring some originally underweight elements over the cap. Also, you haven't mentioned it explicitly but based on the finance tag and your example, I'm assuming that the sum of the list needs to stay constant.
First, I have created a pandas data frame that contains 20 values summing to 1 and ordered it in descending order.
Elements Uncon
3 D 0.081778
1 B 0.079887
19 T 0.079451
17 R 0.070283
11 L 0.068052
4 E 0.057335
12 M 0.054695
5 F 0.051099
6 G 0.049873
18 S 0.049469
14 O 0.045059
16 Q 0.043583
8 I 0.041186
2 C 0.036802
7 H 0.036315
13 N 0.035440
0 A 0.034311
15 P 0.031519
10 K 0.027173
9 J 0.026689
Second, let's set the cap to 0.06. This will make this a good test case for the multiple overweight elements problem you are facing.
cap = 0.06
weights = df.Uncon
Below is the iterative script.
# Obtain constrained weights
constrained_wts = np.minimum(cap, weights)
# Locate all stocks with less than max weight
nonmax = constrained_wts.ne(cap)
# Calculate adjustment factor - this is proportional to original weights
adj = ((1 - constrained_wts.sum()) *
weights.loc[nonmax] / weights.loc[nonmax].sum())
# Apply adjustment to obtain final weights
constrained_wts = constrained_wts.mask(nonmax, weights + adj)
# Repeat process in loop till conditions are satisfied
while ((constrained_wts.sum() < 1) or
(len(constrained_wts[constrained_wts > cap]) >=1 )):
# Obtain constrained weights
constrained_wts = np.minimum(cap, constrained_wts)
# Locate all stocks with less than max weight
nonmax = constrained_wts.ne(cap)
# Calculate adjustment factor - this is proportional to original weights
adj = ((1 - constrained_wts.sum()) *
constrained_wts.loc[nonmax] / weights.loc[nonmax].sum())
# Apply adjustment to obtain final weights
constrained_wts = constrained_wts.mask(nonmax, constrained_wts + adj)
The results can then be reassigned to the data frame and compared.
df['Cons'] = constrained_wts
Elements Uncon Cons
0 A 0.034311 0.039189
1 B 0.079887 0.060000
2 C 0.036802 0.042034
3 D 0.081778 0.060000
4 E 0.057335 0.060000
5 F 0.051099 0.058364
6 G 0.049873 0.056964
7 H 0.036315 0.041478
8 I 0.041186 0.047041
9 J 0.026689 0.030483
10 K 0.027173 0.031037
11 L 0.068052 0.060000
12 M 0.054695 0.060000
13 N 0.035440 0.040479
14 O 0.045059 0.051465
15 P 0.031519 0.036001
16 Q 0.043583 0.049780
17 R 0.070283 0.060000
18 S 0.049469 0.056502
19 T 0.079451 0.060000
Element M is a good example of why a repetitive process is required. It was originally underweight but so close to the cap that it would have become overweight after the first redistribution.