Алгоритм справедливого распределения доходов

Мне нужен алгоритм (желательно абстрактный или в очень понятном коде Python или PHP), который позволяет справедливо распределять выручку как в краткосрочной, так и в долгосрочной перспективе, исходя из следующих ограничений:

  1. Каждая входящая сумма будет целым числом.
  2. Каждое распределение также будет целым числом.
  3. Каждый человек в распределении должен получать долю дохода, выраженную в фиксированной доле. Эти дроби, конечно, могут быть помещены под общий знаменатель (представьте себе целые проценты со знаменателем 100). Сумма всех числителей равна знаменателю (100 в случае процентов).
  4. Чтобы сделать это справедливым в краткосрочной перспективе, человек i должен получать либо floor(revenue*cut[i]), либо ceil(revenue*cut[i]). РЕДАКТИРОВАТЬ: Позвольте мне подчеркнуть, что ceil(revenue*cut[i]) не обязательно равно 1+floor(revenue*cut[i]), поэтому некоторые алгоритмы не работают, в том числе один из представленных (см. мой комментарий к ответу Андрея).
  5. Чтобы сделать это справедливым в долгосрочной перспективе, по мере накопления платежей фактические полученные сокращения должны быть как можно ближе к теоретическим сокращениям, предпочтительно всегда менее 1 единицы. В частности, если общий доход кратен знаменателю общей дроби, каждый человек должен был получить соответствующее кратное его числителю.
  6. [Edit] Забыл добавить, что общая сумма, распределяемая каждый раз, конечно же, должна соответствовать полученной входящей сумме.

Вот пример для наглядности. Предположим, есть три человека, между которыми распределяется доход, и один должен получить 31%, другой - 21%, а третий должен получить 100–31–21 = 48%.

А теперь представьте, что есть доход в 80 монет. Первый человек должен получить монеты floor(80*31/100) или ceil(80*31/100), второй человек должен получить либо floor(80*21/100), либо ceil(80*21/100), а третий человек - либо floor(80*48/100), либо ceil(80*48/100).

А теперь представьте, что появился новый доход в 120 монет. Каждый человек должен снова получить либо нижний, либо верхний предел своих соответствующих сокращений, но поскольку общий доход (200) является кратным знаменателю (100), теперь каждый человек должен иметь свои точные сокращения: у первого человека должно быть 62 монеты , у второго человека должно быть 42 монеты, а у третьего - 96 монет.

Я думаю, что у меня есть алгоритм, рассчитанный на случай распределения доходов между двумя людьми. Это выглядит так (для 35% и 65%):

set TotalRevenue to 0
set TotalPaid[0] to 0
{ TotalPaid[1] is implied and not necessary for the algorithm }
set Cut[0] to 35
{ Cut[1] is implied and not necessary for the algorithm }
set Denominator to 100
each time a payment is received:
   let Revenue be the amount received this time
   set TotalRevenue = TotalRevenue + Revenue
   set tmp = floor(TotalRevenue*Cut[0]/Denominator)
   pay person 0 the amount of tmp - TotalPaid[0]
   set TotalPaid[0] = tmp
   pay person 1 the amount of TotalRevenue - TotalPaid[0]

Я думаю, что этот простой алгоритм удовлетворяет всем моим ограничениям, но я не нашел способа распространить его на более чем двух человек, не нарушив хотя бы одного. Может ли кто-нибудь придумать расширение для нескольких человек (или, возможно, доказать его невозможность)?


person Pedro Gimeno    schedule 19.12.2013    source источник
comment
Интересная проблема ... Всегда ли поступающие суммы распределяются по всему набору людей? И, если говорить о полном наборе, можно ли регулировать процент отсечения между раундами (например, человек i получает 10% в одном раунде, но 17% в следующем раунде)?   -  person NealB    schedule 20.12.2013
comment
В моем случае использования всегда до полного набора, и изменение процентов означало бы сброс системы: они не могут быть изменены на лету. Я не хочу это так сильно усложнять.   -  person Pedro Gimeno    schedule 20.12.2013


Ответы (2)


Думаю, это работает:

  • Следите за totalReceived - общей суммой денег, поступившей в систему.
  • Когда деньги добавляются в систему, добавляйте их в totalReceived.
  • Установите person[i].newTotal = floor(totalReceived * cut[i]). Изменить: здесь произошла ошибка; спасибо комментаторам. Это распределяет часть денег. Следите за тем, сколько денег, чтобы знать, сколько «новых денег» осталось. Чтобы было ясно, мы отслеживаем, какая часть общей суммы денег остается, и интуитивно мы перераспределяем всю сумму на каждом этапе, хотя на самом деле ни один фонд никогда не падает между одним шаг и следующий.
  • Вы должны знать, как распределить оставшуюся целую сумму денег: должно остаться меньше numPeople суммы. Для i в диапазоне от 0 до leftOverMoney - 1 увеличьте person[i].newTotal на 1. Фактически мы даем доллар каждому из первых leftOverMoney человек.
  • Чтобы вычислить, сколько человек «получил» на этом этапе, вычислите person[i].newTotal - person[i].total.
  • Для очистки / доработки установите person[i].total = person[i].newTotal.
person Andrey Mishchenko    schedule 19.12.2013
comment
Для максимальной справедливости распределите остаток в первую очередь тем людям с наибольшей ошибкой распределения. Т.е. на шаге 3 также установите person [i] .error на фракцию (totalReceived * cut [i]). Затем на шаге 4 отсортируйте людей по .error по убыванию, прежде чем дать монетку каждому из первых людей leftOverMoney. - person A. I. Breveleri; 20.12.2013
comment
@ A.I.Breveleri Вы должны быть осторожны, чтобы никто newTotal не оказался меньше его total, если вы сделаете подобное изменение. И я думаю, что это не очевидно ... на самом деле я думаю, что это неправда. - person Andrey Mishchenko; 20.12.2013
comment
Неверно указать person [i] .newTotal = person [i] .total + floor (totalReceived * cut [i]). Если Педро Гимено попытается использовать этот расчет, его распределенные суммы будут слишком большими. Вы, вероятно, смотрите на totalReceived и думаете, что он представляет собой деньги, добавленные в систему для текущего распределения. Но согласно шагам 1 и 2, это общая сумма денег, добавленная в систему для всех распределений на данный момент, включая текущее распределение. - person A. I. Breveleri; 20.12.2013
comment
Пожалуйста. Я не знаю, где у вас этот алгоритм, но он верен. Это метод, который фактически используется финансовыми инвестиционными компаниями для распределения частичных платежей из долгосрочного фонда. Пока частичная выплата положительна и значения в cut [i] не меняются, чей-либо newTotal в конечном итоге оказывается меньше, чем его общая сумма. - person A. I. Breveleri; 20.12.2013
comment
Я только что это придумал. Кажется, самое очевидное, что нужно сделать. - person Andrey Mishchenko; 20.12.2013
comment
Без изменения первого комментария А. И. Бревелери я думаю, что предложенный алгоритм может нарушить одно из ограничений, а именно то, что на каждом шаге каждый получает либо пол, либо потолок соответствующего разреза. Пример: если сокращения составляют 20%, 37% и 43%, а входящая сумма в начале равна 5, у нас есть пол (5 * 20/100) = ceil (5 * 20/100) = 1 и дается дополнительный монета для этого человека нарушит ограничение. С предлагаемой модификацией сортировки по убыванию ошибки я не уверен, но, похоже, это может сработать. Кто-нибудь может это прокомментировать? - person Pedro Gimeno; 20.12.2013
comment
@PedroGimeno хороший контрпример. Я думаю, что если вы отслеживаете, должны ли люди после первого шага, и вы просто пропускаете их на дополнительном шаге монеты, если это не так, то это сработает. - person Andrey Mishchenko; 21.12.2013
comment
Вопрос в том, всегда ли количество людей, имеющих задолженность ›= монет, ожидает распределения в этом раунде. Это действительно так? В противном случае мои требования могут оказаться невыполнимыми. ИЗМЕНИТЬ Конечно, это гарантировано. Ожидающие дроби всегда меньше 1, поэтому их сумма всегда должна быть меньше общей суммы для распределения. С этой модификацией я приму ваш ответ. - person Pedro Gimeno; 21.12.2013
comment
Гм, это не так очевидно, если подумать еще раз. Мои рассуждения не применимы к предложенному вами алгоритму; голова запуталась. Было бы приветствоваться доказательство того, что всегда будет достаточно людей с ожидающими дробями, чтобы получить монеты, ожидающие распределения. - person Pedro Gimeno; 21.12.2013
comment
У меня есть контрпример, показывающий, что этот алгоритм не работает. Если сокращение составляет 31%, 59% и 10%, и первый платеж составляет 9 монет, а второй платеж - 1 монету, человек с 59% должен заплатить 1 монету в соответствии с алгоритмом. потому что person[i].newTotal равно 6 в первый раз (5 плюс увеличение) и 5 ​​во второй раз (потому что продукт по-прежнему равен 5 и во второй раз увеличения нет). - person Pedro Gimeno; 06.03.2014
comment
@Pedro Gimeno: Вы неправильно применили алгоритм. Остальные монеты должны быть распределены среди инвесторов в порядке от наибольшей к наименьшей ошибке распределения. Целочисленное распределение первой выплаты в 9 монет приводит к мистеру 59% = 5 (ошибка 0,31), мистеру 31% = 2 (ошибка 0,79) и мистеру 10% = 0 (ошибка 0,9), при этом остается 2 монеты. . Затем 2 монеты присуждаются Мистеру 10% и Мистеру 31%, потому что их ошибки больше, чем Мистер 59%. Таким образом, окончательное распределение 9 монет: Мистер 59% = 5, Мистер 31% = 3 и Мистер 10% = 1. - person A. I. Breveleri; 29.10.2017
comment
@Pedro Gimeno: Точно так же вторая выплата в 1 монету приводит к общему количеству выплат до 10 монет, распределенных как Мистер 59% = 5 (ошибка 0,9), Мистер 31% = 3 (ошибка 0,1) и Мистер 10% = 1 (ошибка 0), осталась 1 монета. Эта оставшаяся монета присуждается инвестору с наибольшей ошибкой распределения, г-н 59%. Таким образом, окончательное распределение 10 монет: Мистер 59% = 6, Мистер 31% = 3 и Мистер 10% = 1. Фонд выписывает чек Мистеру 59% за 1 монету; Мистер 31% и мистер 10% ничего не получают от второй выплаты. Никто и никогда не обязан возвращать деньги. - person A. I. Breveleri; 29.10.2017
comment
@ A.I.Breveleri: Я применил алгоритм, который показан в ответе Андрея Мищенко, в котором нет этого дополнительного шага. Ответ, как указано, не работает. Я не оценивал алгоритм с учетом вашего изменения, но я оценю, если вы добавите его в качестве ответа или если Андрей изменит ответ, добавив этот шаг, и приму его, если он работает. - person Pedro Gimeno; 30.10.2017
comment
@ Педро Гимено: Согласен. Ты прав. Ваш контрпример приводит к сбою алгоритма Андрея без упорядочивания ошибок распределения. \ FWIW в реальной жизни алгоритм немодифицированный используется в течение десятилетий для округления дивидендов до равных долларов, не вызывая проблемы возврата, потому что (1) выплаты очень велики по сравнению с количеством инвесторов и ( 2) за автоматическим расчетом в любом случае следует этап ручного просмотра и настройки, где обратный ход - лишь одна из многих проблем, которые необходимо исправить. - person A. I. Breveleri; 30.10.2017

Чтобы учесть ограничение 4, текущий платеж должен использоваться в качестве основы для распределения. После распределения округленных дивидендов среди инвесторов оставшиеся доллары могут быть присуждены, по одному каждому, любому подходящему инвестору. Инвестор имеет право на участие, если ему полагается дробная доля, то есть ceil (доход [i])> нижний предел (доход [i]).

Ограничение 5 можно устранить, решив, кто из подходящих инвесторов должен получить оставшиеся доллары. Максимум долгосрочной справедливости будет наиболее близок к тому, чтобы всегда распределять оставшиеся доллары правомочными инвесторами, которые накопили наибольшую (долгосрочную) ошибку распределения.

Следующее основано на алгоритме, который я использовал для своих клиентов, управляющих фондами. (Я всегда использовал Паскаль для финансовых приложений по причинам криминалистики.) Я перенес фрагменты кода из приложения базы данных в метод, основанный на массивах, в попытке улучшить ясность.

В структуру данных бухгалтерского учета включена таблица инвесторов. В информацию о каждом инвесторе включена доля его доли (количество долей инвестора, деленное на общее количество долей фонда), сумма, заработанная на текущий момент, и распределенная сумма (фактически выплаченная инвестору) на текущий момент.

Investors : array [0..InvestorCount-1] of record
        // ... lots of information about this investor
        ShareFraction       : float; // >0 and <1
        EarnedToDate        : currency; // rounded to dollars
        DistributedToDate   : currency; // rounded to dollars
    end;
// The sum of all the .ShareFraction in the table is exactly 1.

Для этих расчетов создаются временные данные, которые впоследствии освобождаются.

DistErrs : array [0..InvestorCount-1] of record
        TotalError      : float; // distribution rounding error
        CurrentError    : float; // distribution rounding error
        InvestorIndex   : integer; // link to Investors record
    end;
UNDISTRIBUTED_PAYMENTS  : currency;
CurrentFloat            : float;
CurrentInteger          : currency;
CurrentFraction         : float;
TotalFloat              : float;
TotalInteger            : currency;
TotalFraction           : float;
DD, II                  : integer; // array indices

FUND_PREVIOUS_PAYMENTS : currency; - параметр, полученный из истории фонда. Если он не указан в качестве параметра, он может быть рассчитан как сумма по всем Investors []. EarnedToDate.

FUND_CURRENT_PAYMENT : currency; - параметр, полученный из текущего увеличения стоимости фонда.

FUND_TOTAL_PAYMENTS : currency; - параметр, производный от текущей стоимости фонда. Это общая сумма ранее выплаченного фонда плюс текущий платеж фонда.

Эти три значения образуют зависимую систему с двумя степенями свободы. Могут быть предоставлены любые два, а третий рассчитывается исходя из других.

// Calculate how much each investor has earned after the fund current payment.
UNDISTRIBUTED_PAYMENTS := FUND_CURRENT_PAYMENT;
for II := 0 to InvestorCount-1 do begin

  // Calculate theoretical current dividend whole and fraction parts.
  CurrentFloat := FUND_CURRENT_PAYMENT * Investors[II].ShareFraction;
  CurrentInteger := floor(CurrentFloat);
  CurrentFraction := CurrentFloat - CurrentInteger;

  // Calculate theoretical total dividend whole and fraction parts.
  TotalFloat := FUND_TOTAL_PAYMENTS * Investors[II].ShareFraction;
  TotalInteger := floor(TotalFloat);
  TotalFraction := TotalFloat - TotalInteger;

  // Distribute the current whole dollars to the investor.
  Investors[II].EarnedToDate := Investors[II].EarnedToDate + CurrentInteger;
  // Track total whole dollars distributed.
  UNDISTRIBUTED_PAYMENTS := UNDISTRIBUTED_PAYMENTS - CurrentInteger;

  // Record the fractional dollars in the errors table and link to investor.
  DistErrs[II].CurrentError := CurrentFraction;
  DistErrs[II].TotalError := TotalFraction;
  DistErrs[II].InvestorIndex := II;

end;

В этот момент UNDISTRIBUTED_PAYMENTS всегда меньше InvestorCount.

// Sort DistErrs by .TotalError descending.

В реальном мире данные хранятся в базе данных, а не в массивах, поэтому сортировка выполняется путем создания индекса по DistErrs с использованием TotalError в качестве ключа.

// Distribute one dollar each to the first UNDISTRIBUTED_PAYMENTS eligible 
// investors (i.e. those whose current share ceilings are higher than their 
// current share floor), in order from greatest to least .TotalError.
for DD := 0 to InvestorCount-1 do begin
if UNDISTRIBUTED_PAYMENTS <= 0 then break;
  if DistErrs[DD].CurrentError <> 0 then begin
    II := DistErrs[DD].InvestorIndex;
    Investors[II].EarnedToDate := Investors[II].EarnedToDate + 1;
    UNDISTRIBUTED_PAYMENTS := UNDISTRIBUTED_PAYMENTS - 1;
  end;
end;

В последующем процессе каждому инвестору выплачивается разница между его .EarnedToDate и его .DistributedToDate, а его .DistributedToDate корректируется, чтобы отразить этот платеж.

person A. I. Breveleri    schedule 31.10.2017
comment
Спасибо за попытку. К сожалению, и этот алгоритм не удовлетворяет ограничениям. Чтобы исключить проблемы округления с плавающей запятой, я попытался использовать 32 акции, причем у первого инвестора было 3, у второго инвестора было 7, а у третьего инвестора было 32-3-7 = 22. Затем два платежа: первый 379, затем 244. В последнем платеже четвертое ограничение нарушается, так как он платит 24 монеты первому инвестору, когда правильное значение должно быть между 22 и 23 (реальное значение - 22,875). Вот моя программа: formauri.es/personal/pgimeno/pastes/invest.py < / а> - person Pedro Gimeno; 01.11.2017
comment
Я изменил свой алгоритм, чтобы удовлетворить вашему ограничению 4, максимально приближаясь к вашему ограничению 5. В последнем цикле пропуск любого инвестора с CurrentError = 0 позволяет избежать нарушения его потолка, а предварительный заказ по убыванию TotalError максимизирует долгосрочную справедливость. - person A. I. Breveleri; 04.11.2017
comment
Я проверил ваш модифицированный алгоритм. Это все еще не соответствует ограничению 5. При 3/32, 7/32 и 22/32, как и раньше, платеж 82, за которым следует платеж в размере 263, приводит к тому, что 3-й инвестор получает накопленную стоимость 236, тогда как он должен был получить 237,1875. OTOH, первая выплата в размере 404, за которой следуют 19, приводит к тому, что 3-й инвестор получает 292, тогда как они должны были получить 290,8125. Я думаю, что наилучшее оперативное определение как можно ближе к ограничению 5 - это расстояние от пола до потолка, иначе говоря, менее 1 монеты. Мой модифицированный алгоритм выше, кажется, доказывает, что это возможно. - person Pedro Gimeno; 05.11.2017
comment
Вот моя программа на случай, если я ошибся: formauri.es/personal /pgimeno/pastes/invest-new.py - person Pedro Gimeno; 05.11.2017