Сходимся к наилучшему сочетанию элементов

У вас есть 10 000 долларов, которые вы можете инвестировать в акции. Вам дается список из 200 акций, и вам предлагается выбрать 8 из них для покупки, а также указать, сколько из этих акций вы хотите купить. Вы не можете потратить более 2500 долларов на одну акцию, и каждая акция имеет свою цену в диапазоне от 100 до 1000 долларов. Вы не можете купить часть акций, только целые числа. К каждой акции также привязана стоимость, указывающая, насколько она прибыльна. Это произвольное число от 0 до 100, которое служит простой системой оценки.

Конечная цель состоит в том, чтобы составить список оптимального выбора из 8 акций и указать наилучшее количество каждой из этих акций для покупки, не превышая лимита в 2500 долларов для каждой акции.

• Я не прошу советов по инвестированию, я выбрал акции, потому что они служат хорошей метафорой реальной проблемы, которую я пытаюсь решить.

• Похоже, я рассматриваю более сложную версию задачи о рюкзаке 0/1: https://en.wikipedia.org/wiki/Knapsack_problem.

• Нет, это не домашнее задание.


person Kael Eppcohen    schedule 21.02.2018    source источник
comment
Вы хотите лучше понять, как на самом деле инвестировать, или вы ищете, как максимизировать свою произвольную формулу? Если вы действительно хотите инвестировать, вы хотите максимизировать альфу, сохраняя при этом бета на приемлемом уровне. См. investopedia.com/ask/answers. /102714/ для основных определений этих терминов.   -  person btilly    schedule 21.02.2018
comment
@btilly Не заинтересован в инвестировании, но использовал акции как самодостаточную метафору для реальной проблемы, которую я пытаюсь решить.   -  person Kael Eppcohen    schedule 21.02.2018
comment
В таком случае, не могли бы вы предложить всю проблему? С какой информации вы начинаете, какую формулу пытаетесь максимизировать? (Я знаю, что цена является частью этого, но это явно указано в вашем текущем описании.)   -  person btilly    schedule 21.02.2018
comment
@btilly полностью переписал задачу, чтобы она была более конкретной и содержала меньше описания.   -  person Kael Eppcohen    schedule 21.02.2018
comment
Что вы пытаетесь максимизировать? Сумма прибыли вашего портфеля?   -  person btilly    schedule 21.02.2018
comment
@btilly, да, сумма оценок прибыли каждой акции * количество акций, которые вы решили купить. Таким образом, если S — оценка акции, а Q — количество, сумма S*Q для каждой из 8 выбранных вами акций — это то, что вы оптимизируете.   -  person Kael Eppcohen    schedule 21.02.2018


Ответы (1)


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

#! /usr/bin/env python
from collections import namedtuple

Stock = namedtuple('Stock', ['id', 'price', 'profit'])

def optimize (stocks, money=10000, max_stocks=8, max_per_stock=2500):
    Investment = namedtuple('investment', ['profit', 'stock', 'quantity', 'previous_investment'])
    investment_transitions = []
    last_investments = {money: Investment(0, None, None, None)}
    for _ in range(max_stocks):
        next_investments = {}
        investment_transitions.append([last_investments, next_investments])
        last_investments = next_investments


    def prioritize(stock):
        # This puts the best profit/price, as a ratio, first.
        val = [-(stock.profit + 0.0)/stock.price, stock.price, stock.id]
        return val

    for stock in sorted(stocks, key=prioritize):
        # We reverse transitions so we have not yet added the stock to the
        # old investments when we add it to the new investments.
        for transition in reversed(investment_transitions):
            old_t = transition[0]
            new_t = transition[1]
            for avail, invest in old_t.iteritems():
                for i in range(int(min(avail, max_per_stock)/stock.price)):
                    quantity = i+1
                    new_avail = avail - quantity*stock.price
                    new_profit = invest.profit + quantity*stock.profit
                    if new_avail not in new_t or new_t[new_avail].profit < new_profit:
                        new_t[new_avail] = Investment(new_profit, stock, quantity, invest)
    best_investment = investment_transitions[0][0][money]
    for transition in investment_transitions:
        for invest in transition[1].values():
            if best_investment.profit < invest.profit:
                best_investment = invest

    purchase = {}
    while best_investment.stock is not None:
        purchase[best_investment.stock] = best_investment.quantity
        best_investment = best_investment.previous_investment

    return purchase


optimize([Stock('A', 100, 10), Stock('B', 1040, 160)])

И вот с крошечной оптимизацией удаления инвестиций, как только мы видим, что дальнейшее добавление акций к нему не может улучшиться. Это, вероятно, будет работать на несколько порядков быстрее, чем старый код с вашими данными.

#! /usr/bin/env python
from collections import namedtuple

Stock = namedtuple('Stock', ['id', 'price', 'profit'])

def optimize (stocks, money=10000, max_stocks=8, max_per_stock=2500):
    Investment = namedtuple('investment', ['profit', 'stock', 'quantity', 'previous_investment'])
    investment_transitions = []
    last_investments = {money: Investment(0, None, None, None)}
    for _ in range(max_stocks):
        next_investments = {}
        investment_transitions.append([last_investments, next_investments])
        last_investments = next_investments


    def prioritize(stock):
        # This puts the best profit/price, as a ratio, first.
        val = [-(stock.profit + 0.0)/stock.price, stock.price, stock.id]
        return val

    best_investment = investment_transitions[0][0][money]
    for stock in sorted(stocks, key=prioritize):
        profit_ratio = (stock.profit + 0.0) / stock.price
        # We reverse transitions so we have not yet added the stock to the
        # old investments when we add it to the new investments.
        for transition in reversed(investment_transitions):
            old_t = transition[0]
            new_t = transition[1]
            for avail, invest in old_t.items():
                if avail * profit_ratio + invest.profit <= best_investment.profit:
                    # We cannot possibly improve with this or any other stock.
                    del old_t[avail]
                    continue
                for i in range(int(min(avail, max_per_stock)/stock.price)):
                    quantity = i+1
                    new_avail = avail - quantity*stock.price
                    new_profit = invest.profit + quantity*stock.profit
                    if new_avail not in new_t or new_t[new_avail].profit < new_profit:
                        new_invest = Investment(new_profit, stock, quantity, invest)
                        new_t[new_avail] = new_invest
                        if best_investment.profit < new_invest.profit:
                            best_investment = new_invest

    purchase = {}
    while best_investment.stock is not None:
        purchase[best_investment.stock] = best_investment.quantity
        best_investment = best_investment.previous_investment

    return purchase
person btilly    schedule 21.02.2018
comment
Это выглядит супер многообещающе! Мне нужно немного ознакомиться с Python, чтобы воссоздать его на Java. Я заметил одну вещь: если вы используете Python 3.0 или выше, вы получите сообщение об ошибке. Это связано с тем, что Python 3.0 переименовал iteritems() в items(). - person Kael Eppcohen; 22.02.2018
comment
@KaelEppcohen Не хватает очень важной оптимизации. Это, с вашими данными, потенциально должно выполнять сотни миллионов операций. (10000 долларов, умноженные на 8 возможных количеств покупок, умноженных на 25, умноженных на одну акцию.) Я добавлю это, и внезапно это будет работать намного быстрее. - person btilly; 22.02.2018
comment
Новый код оптимизации работает, но только в том случае, если я внесу следующие коррективы: для avail инвестируйте в list(old_t.items()): вместо исходного для avail инвестируйте в old_t.items(): - person Kael Eppcohen; 22.02.2018
comment
@KaelEppcohen А, это имеет смысл. Не следует удалять из хэша во время его повторения. :-) - person btilly; 22.02.2018
comment
Я совершенно новичок в Python, и мне трудно понять, зачем инвестировать в old_t.items(): на самом деле делает. old_t содержит предыдущий переход, который произошел, верно? И этот переход содержит две инвестиции, исходную и ту, на которую мы перешли? Итак, когда вы вызываете avail, инвестируйте в old_t.items(): на самом деле вы просто выполняете итерацию дважды, первая итерация предназначена для первоначальных инвестиций, а вторая — та, на которую мы переключились, верно? - person Kael Eppcohen; 22.02.2018
comment
@KaelEppcohen Нет. Словарь Python — это Java HashMap. Когда вы выполняете итерацию с ForEach, вы получаете <K, V> пар. То же самое в Питоне. Чтобы быть более конкретным, avail — это сумма в долларах, которую осталось инвестировать, и это ключ. invest фактически представляет собой LinkedList, содержащий подробную информацию об инвестициях. - person btilly; 22.02.2018
comment
Итак, после долгого изучения синтаксиса Python (и многому научившись по ходу дела) я смог воссоздать эту функцию на Java. Спасибо, что предоставили такое отличное решение и помогли мне его понять! - person Kael Eppcohen; 22.02.2018
comment
@KaelEppcohen Из любопытства, как быстро он работал с вашими данными? - person btilly; 23.02.2018
comment
в версии Java, которую я воссоздал, я смог запустить 300 акций со случайно сгенерированными ценами и прибылью 1-1000 за 5-15 секунд как для версий Java, так и для версий Python. Это с 10k денег, 2,5 запаса капитала, 8 инвестиций макс. Когда я масштабирую задействованные переменные, время, необходимое для этого, увеличивается экспоненциально. Я собираюсь найти способ многопоточности, но могут потребоваться дальнейшие оптимизации/ограничения/ярлыки, чтобы сделать этот масштаб до верхних границ того, для чего я планирую его использовать. - person Kael Eppcohen; 23.02.2018
comment
РЕДАКТИРОВАТЬ: кажется, что время, которое требуется, является ОЧЕНЬ контекстуальным, а это означает, что диапазон, в котором работают ваши данные, сильно влияет на время, необходимое для обработки. Кроме того, кажется, что алгоритм может быть смещен в сторону выбора более дорогих акций, но для подтверждения требуется дальнейшее тестирование. - person Kael Eppcohen; 23.02.2018
comment
@KaelEppcohen Да, это зависит от данных. Вот почему он называется псевдополиномиальным. Если вы округлите цены до ближайших, скажем, 50 долларов, вы можете увидеть гораздо лучшую производительность. - person btilly; 23.02.2018
comment
@KaelEppcohen Вот важная деталь. Это отдает приоритет акциям с наилучшей ценой в первую очередь. Поэтому, если вы запустите его, скажем, на 1 секунду, то все, до чего он дошел, вероятно, является довольно хорошим решением. Возможно что-то в конце может стать улучшением. Но это довольно маловероятно. - person btilly; 23.02.2018
comment
о ничего себе вы не шутите насчёт округления. Я получил ошибку памяти в одном случае, когда я не округлял, и после применения относительно незначительного округления он обработал его менее чем за 1 секунду. Это чудесно. - person Kael Eppcohen; 23.02.2018