Използване на дървета на решенията в Python за извличане на представа за решението на A да се премести в Лас Вегас

Съвсем наскоро собственикът на бейзболния отбор Оукланд Атлетикс, Джон Фишер, обяви, че отборът е закупил близо 50 акра земя в Лас Вегас, Невада. [1] Това поставя бъдещето на последния останал професионален спортен отбор в Оукланд в опасност. През последните 5 години Оукланд видя Голдън Стейт Уориърс (НБА) и Лас Вегас Рейдърс (НФЛ) да заминат за по-нови, по-лъскави стадиони в други градове (въпреки че Голдън Стейт току-що премина през Bay Bridge до Сан Франциско). Докато процесът на вземане на решения във фронт офиса на Оукланд А остава загадка за мен, науката за данните и анализът на решенията в тандем могат да разкрият много за мотивите на Джон Фишер да се премести в Лас Вегас.

Анализът на решенията е важен за разбиране от всички специалисти по данни, защото той е мостът между високотехническата работа на вероятностните и статистическите модели и бизнес решенията. Разбирането на начина, по който се вземат бизнес решения, може да помогне за рамкирането на нашата работа и представянето на нашите констатации на нетехнически аудитории, докато предоставяме приложими препоръки и констатации. Институтът за изследване на операциите и науката за управление (INFORMS) дори има „цяло общество, посветено на анализа на решенията“.

Освен това машинното обучение може да помогне за обобщаване на резултатите от анализа на решенията чрез отключване на прозрения от вероятностни анализи на чувствителността. След първоначалното конструиране на модел, разчленяващ сценариите на Оукланд срещу Лас Вегас, използвайки анализ на решенията, ние ще използваме машинно обучение, за да копаем за модели, които биха могли да помогнат за разкриването на приложими препоръки за A, ако обстоятелствата на решението се променят.

Какво е анализ на решения?

Анализът на решенията е областта на изследване, посветена на „систематичен, количествен и визуален подход към адресирането и оценката на важни избори“. [2] Той може да бъде мощен инструмент в среда с малко данни и да помогне на хората да използват комбинация от експертни познания и предварителни знания, за да подобрят очакваната стойност на сложни решения. Използва се в широк спектър от области, като икономика, управление и анализ на политиката.

Като цяло, в света на анализа на решенията, ние приемаме Байесова перспектива на света. Основната теорема на Байс е следната:

Където P(A) е вероятността за настъпване на събитие A, P(B) е вероятността за настъпване на събитие B, P(A|B) е вероятността за настъпване на събитие A, като се има предвид, че събитие B е настъпило, и P(B|A ) е вероятността събитие B да се случи, като се има предвид, че събитие A се е случило. Обикновено P(A) представлява предишно убеждение относно шанса A да се случи, а B представлява някои нови данни. P(A|B) е актуализирано постериорно вярване за шанса А да се случи, след като сте наблюдавали B.

Например, да предположим, че отиваме в Oakland-Alameda County Coliseum, за да гледаме мач с топка, но не сме следили статистиката на играчите. Започваме със знанието, че външните играчи влизат в база с вероятност 0,35, вътрешните играчи влизат в база с вероятност 0,25, а избраните нападатели влизат в база с вероятност 0,4. Нека A е събитието, при което следващият удар е външен играч, B е събитието, при който следващият удар е вътрешен играч, а C е събитието, при който следващият удар е определеният играч. Тъй като знаем списъка на бейзболния отбор, вече знаем, че P(A) = 0,33, P(B) = 0,56 и P(C) = 0,11. Сега, това следващо тесто идва в чинията и за наша радост, попада на база (събитие D)! От предишните ни познания за бейзбола знаем, че P(D|A) = 0,35, P(D|B) = 0,25 и P(D|C) = 0,4. Използвайки закона за пълната вероятност, можем да изчислим P(D) = P(D|A)P(A) + P(D|B)P(D) + P(D|C)P(C) = 0,3. Сега можем да актуализираме нашите вярвания за това кой тип играч е бил ударът: P(A|D) = 0,39, P(B|D) = 0,47 и P(C|D) = 0,15. След като видяхме играча да се качва на базата, сега е по-вероятно да повярваме, че играчът не е бил infielder. Сега, когато сте в правилното настроение, нека продължим.

Ключовият инструмент в анализа на решенията е дървото на решенията (да не се бърка с алгоритъма за машинно обучение със същото име). [3] Дървото на решенията включва два основни компонента: възел за решение и възел за избор. [3] В този блог ще ви покажа как да конструирате дърво на решенията, да го оцените в Python и да разберете решението на Oakland A да се премести в Лас Вегас.

Какво е решението?

Хауърд и Абас определят решение като „избор между две или повече алтернативи, който включва неотменимо разпределение на ресурси“. [3] Това е широко определение, но в нашия пример за Оукланд А решението е: трябва ли бейзболният отбор на Атлетикс да остане в Оукланд или да се премести в Лас Вегас? В този случай решението е неотменимо, защото те ще строят нов стадион, независимо от избрания град.

Какви са несигурностите?

Несигурността заобикаля всяко решение, което вземаме. В решението дали да останат в Оукланд или да се преместят в Лас Вегас, А не са сигурни относно разходите за нови стадиони и последващите оперативни приходи: 1) колко публични пари ще получат, за да финансират новия си стадион, 2) колко приходи ще получат генерират от продажба на билети и 3) колко приходи ще генерират от сделки за местна телевизия.

В момента A's се надяват да построят стадион за 1,5 милиарда долара в Лас Вегас. [1] Още през 2021 г. организацията поиска 855 милиона долара публични средства, за да помогне за изграждането на техния нов стадион в Оукланд, въпреки че преди това се съгласи с града и окръга, че новият стадион в Оукланд ще бъде частно финансиран. [1] Следователно можем разумно да предположим, че разходите за изграждане на стадиона са приблизително еднакви и в двете населени места. Единствената несигурност тук е колко пари на данъкоплатците ще отидат за финансиране на стадиона.

Прогнозните приходи от продажба на билети варират значително между отборите от $27 милиона до $131 милиона със средно около $75 милиона. [4] Оукланд се оценява на приходи от билети от приблизително 55 милиона долара. [4]

Телевизионните приходи в MLB са равномерно разпределени за национални телевизионни сделки, договорени от MLB. Важен компонент от телевизионните приходи на индивидуалните отбори обаче идва чрез регионалните спортни мрежи (RSN). Екипите запазват голяма част от приходите от местни телевизионни сделки, въпреки че все още има голяма част от споделянето на приходите. След споделянето на приходите, приходите от телевизионни договори от RSN варират от $36 милиона до $131 милиона, като всички екипи, освен най-ценните, правят по-малко от $60 милиона. [4]

Благодарение на преместването на Raiders (NFL) в Лас Вегас от Оукланд преди няколко години, знаем, че град Лас Вегас е бил готов да предостави 750 милиона долара публични средства за изграждането на чисто нов футболен стадион. [5] Знаем също, че както местните жители, така и туристите са готови да се присъединят и да аплодират нов професионален отбор, тъй като Raiders водеха НФЛ по приходи от билети през 2021 г. със 119 милиона долара за годината. [6]

Има методи, които са извън обхвата на този блог, за да поискат предварителни убеждения на вземащия решения относно вероятните резултати от тези несигурности и вероятностите за всяка от тях. Освен това се съмнявам, че Джон Фишер е готов да коментира за моя блог. Така че междувременно ще използвам информацията, която събрах от тези уеб източници, за да предоставя няколко възможни сценария за всяка несигурност.

Какъв е нашият времеви хоризонт за вземане на решения?

Разбира се, приходите са годишни, а стадионът трябва да издържи много повече от година. Времевите хоризонти могат да се различават в зависимост от контекста на решението и от начина, по който лицето, вземащо решение, гледа на вероятността от промяна в пейзажа. От гледна точка на науката за данните, това е в съответствие с отклонението на данните, където данните, използвани за обучение на модела, са различни от текущите данни. Засега нека приемем, че тези оценки ще останат сравнително стабилни през десетилетието и ще използваме 10-годишен времеви хоризонт с 3% дисконтов процент върху нашите годишни разходи.

Как изглежда дървото на решенията?

Сега, след като дефинирахме всички компоненти на нашето дърво на решенията, е време да изградим дървото. Концептуално, ето как изглежда:

Квадратният възел е възелът на решението, кръговите възли са шансовите възли, а триъгълните възли са крайните възли. Поради ограничения на пространството, цялото дърво не се вижда в изображението, но всеки възел също има свързана вероятност и стойност.

Как да изградим модела в Python?

При анализ на решения, след установяване на конструкцията на нашето дърво на решенията, можем да идентифицираме най-доброто решение чрез „връщане назад“ на дървото. В този пример приемаме рационално (известно още като очаквана стойност) лице, вземащо решения. И така, започваме с таблично представяне на стойността, свързана със състоянието на терминала, ако е приложимо. Това ще стане нашата текуща обща или очаквана стойност. В този случай не е приложимо, така че започваме с общо $0. След това итеративно изчисляваме очакваната стойност на всеки набор от възли вляво от крайните възли и я добавяме към текущата обща или очаквана стойност. В крайна сметка ще завършим с една очаквана стойност на решението да останем в Оукланд и една очаквана стойност на решението да се преместим в Лас Вегас.

Нека започнем с проста настройка на нашия базов сценарий. Ще възприемем подхода за създаване на рамка от данни от всички възможни комбинации от решения, публични средства, продажба на билети и сценарии за приходи от RSN.

import numpy as np
import pandas as pd

# Create data frame of all possible outcomes
decision_list = ['Oakland', 'Las Vegas']

# First Node
chance_node_stadium_money_scenarios = ['Optimistic', 'Neutral', 'Pessimistic']
chance_node_stadium_money_probabilities_oakland = [0.1, 0.3, 0.6]
chance_node_stadium_money_probabilities_vegas = [0.5, 0.4, 0.1]
chance_node_stadium_money_values = [855, 500, 0]

#Second Node
chance_node_ticket_sales_scenarios = ['Optimistic', 'Neutral', 'Pessimistic']
chance_node_ticket_sales_probabilities_oakland = [0.2, 0.2, 0.6]
chance_node_ticket_sales_probabilities_vegas = [0.3, 0.4, 0.3]
chance_node_ticket_sales_values_per_year = [80, 55, 27]

# Third Node
chance_node_rsn_revenue_scenarios = ['Optimistic', 'Neutral', 'Pessimistic']
chance_node_rsn_revenue_probabilities_oakland = [0.15, 0.5, 0.35]
chance_node_rsn_revenue_probabilities_vegas = [0.1, 0.3, 0.6]
chance_node_rsn_revenue_values_per_year = [60, 45, 36]

# Convert annual values to NPV of 10 year time horizon
time_horizon = 10 # years
discount_rate = 0.03 # per year
chance_node_ticket_sales_values = [val * (1 - (1/((1 + discount_rate)**time_horizon)))/discount_rate for val in chance_node_ticket_sales_values_per_year]
chance_node_rsn_revenue_values = [val * (1 - (1/((1 + discount_rate)**time_horizon)))/discount_rate for val in chance_node_rsn_revenue_values_per_year]

# Create data frame of all possible scenarios
decision_list_list_for_df = []
chance_node_stadium_money_list_for_df = []
chance_node_stadium_money_probability_list_for_df = []
chance_node_stadium_money_value_list_for_df = []
chance_node_ticket_sales_list_for_df = []
chance_node_ticket_sales_probability_list_for_df = []
chance_node_ticket_sales_value_list_for_df = []
chance_node_rsn_revenue_list_for_df = []
chance_node_rsn_revenue_probability_list_for_df = []
chance_node_rsn_revenue_value_list_for_df = []

for i in decision_list:
    for j in range(len(chance_node_stadium_money_scenarios)):
        for k in range(len(chance_node_rsn_revenue_scenarios)):
            for m in range(len(chance_node_rsn_revenue_scenarios)):
                decision_list_list_for_df.append(i)
                chance_node_stadium_money_list_for_df.append(chance_node_stadium_money_scenarios[j])
                chance_node_stadium_money_value_list_for_df.append(chance_node_stadium_money_values[j])
                chance_node_ticket_sales_list_for_df.append(chance_node_ticket_sales_scenarios[k])
                chance_node_ticket_sales_value_list_for_df.append(chance_node_ticket_sales_values[k])
                chance_node_rsn_revenue_list_for_df.append(chance_node_rsn_revenue_scenarios[m])
                chance_node_rsn_revenue_value_list_for_df.append(chance_node_rsn_revenue_values[m])
                
                if i == 'Oakland':
                    chance_node_stadium_money_probability_list_for_df.append(chance_node_stadium_money_probabilities_oakland[j])
                    chance_node_ticket_sales_probability_list_for_df.append(chance_node_ticket_sales_probabilities_oakland[k])
                    chance_node_rsn_revenue_probability_list_for_df.append(chance_node_rsn_revenue_probabilities_oakland[m])
                elif i == 'Las Vegas':
                    chance_node_stadium_money_probability_list_for_df.append(chance_node_stadium_money_probabilities_vegas[j])
                    chance_node_ticket_sales_probability_list_for_df.append(chance_node_ticket_sales_probabilities_vegas[k])
                    chance_node_rsn_revenue_probability_list_for_df.append(chance_node_rsn_revenue_probabilities_vegas[m])
                    
decision_tree_df = pd.DataFrame(list(zip(decision_list_list_for_df, chance_node_stadium_money_list_for_df,
                                         chance_node_stadium_money_probability_list_for_df,
                                         chance_node_stadium_money_value_list_for_df,
                                         chance_node_ticket_sales_list_for_df,
                                         chance_node_ticket_sales_probability_list_for_df,
                                         chance_node_ticket_sales_value_list_for_df,
                                         chance_node_rsn_revenue_list_for_df,
                                         chance_node_rsn_revenue_probability_list_for_df,
                                         chance_node_rsn_revenue_value_list_for_df)),
                               columns = ['Decision',
                                          'Stadium_Money_Result', 'Stadium_Money_Prob', 'Stadium_Money_Value',
                                          'Ticket_Sales_Result', 'Ticket_Sales_Prob', 'Ticket_Sales_Value', 
                                          'RSN_Revenue_Result', 'RSN_Revenue_Prob', 'RSN_Revenue_Value'])

Сега, ако отпечатате своето дърво на решенията, ще получите рамка от данни на pandas от 54 реда и 10 колони. Можем лесно да върнем назад дървото на решенията с креативно използване на функциите groupby и merge. Нека започнем с таблично представяне на очакваната стойност от приходите от RSN за всяка комбинация от решение, пари на стадиона и продажба на билети:

decision_tree_df['RSN_EV'] = decision_tree_df['RSN_Revenue_Prob'] * decision_tree_df['RSN_Revenue_Value']

# Consolidate the RSN_EV values
RSN_rollback_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob', 'Ticket_Sales_Result', 'Ticket_Sales_Prob'])['RSN_EV'].sum().reset_index()

# Keep the rest of the columns
decision_tree_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob', 'Ticket_Sales_Result', 'Ticket_Sales_Prob'])['Stadium_Money_Value', 'Ticket_Sales_Value'].mean().reset_index()

# merge two dataframes
decision_tree_df = pd.merge(decision_tree_df, RSN_rollback_df, on = ['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob', 'Ticket_Sales_Result', 'Ticket_Sales_Prob'])

Получената таблица се е свила и сега можете визуално да видите очакваната стойност на върнатите RSN възли за приходи.

Повтаряне на процеса с продажбата на билети. Имаме следния код:

decision_tree_df['Ticket_Sales_RSN_EV'] = decision_tree_df['Ticket_Sales_Prob'] * decision_tree_df['Ticket_Sales_Value'] + decision_tree_df['RSN_EV']

# Consolidate the Ticket Sales and RSN_EV values
ticket_sales_rollback_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob'])['Ticket_Sales_RSN_EV'].sum().reset_index()

# Keep the rest of the columns
decision_tree_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob'])['Stadium_Money_Value'].mean().reset_index()

# merge two dataframes
decision_tree_df = pd.merge(decision_tree_df, ticket_sales_rollback_df, on = ['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob'])

И изход:

И накрая, повтаряйки за публичния паричен принос на stdium:

decision_tree_df['Stadium_Money_Ticket_Sales_RSN_EV'] = decision_tree_df['Stadium_Money_Prob'] * decision_tree_df['Stadium_Money_Value'] + decision_tree_df['Ticket_Sales_RSN_EV']

# Consolidate the Stadium Money, Ticket Sales, and RSN_EV values
decision_tree_df = decision_tree_df.groupby(['Decision'])['Stadium_Money_Ticket_Sales_RSN_EV'].sum().reset_index()

Тук можем да видим, че моделът изчислява, че за 10-годишен времеви хоризонт очакваната стойност на оставането в Оукланд е 4,7 милиарда долара, докато очакваната стойност на преместването в Лас Вегас е 5,2 милиарда долара.

Как можем да обобщим модела?

Разбира се, има несигурност както в нашите данни, така и в нашия модел и има много различни сценарии, които можем да тестваме. Естествено, може да се опитаме да определим някои прагове или сценарии, при които решението се променя от оставане в Оукланд към преместване в Лас Вегас (или обратното). Тези точки за вземане на решения могат да служат като полезен набор от „бизнес правила“ за вземащите решения и могат да ни помогнат като специалисти по данни да извлечем приложими препоръки от нашия анализ.

Има много начини да постигнете тази цел, но в този блог ще използваме мета-моделиране на машинно обучение. Метамоделирането включва разработването на по-бърз (и понякога по-прост) модел на оригинален математически или симулационен модел, който приема същите входни данни и произвежда много сходни резултати [7]. В този случай ще използваме вероятностен анализ на чувствителността, за да тестваме голямо пространство от параметри на дървото на решенията за анализ на решения и ще отбележим полученото решение за всеки набор от параметри. След това ще обучим модел за класификация на дървото на решенията за машинно обучение, като използваме набора от параметри като функции и полученото решение като етикети за нашия модел за машинно обучение. Ползата от модела за машинно обучение е, че той ще разкрие за нас сложни връзки, които биха били трудни за дешифриране само с многовариантен анализ на чувствителността. Надеждата е, че можем да получим достатъчно точност от едно плитко дърво, за да опишем сценариите, при които А трябва да останат в Оукланд срещу преместване в Лас Вегас.

Първо, започваме с проектиране на вероятностен анализ на чувствителността. За този пример ще приемем, че доларовите стойности на шансовите възли ще останат същите, но вероятностите за различните резултати ще варират. Тъй като знаем, че вероятностите ще варират между стойности от 0 до 1, ще приемем, че всички сценарийни вероятности са еднакво вероятни и ще ги моделираме с помощта на равномерно разпределение с минимална стойност 0 и максимална стойност 1. След вземане на проби три пъти от равномерното разпределение (един за всеки оптимистичен, неутрален и песимистичен сценарий), ще нормализираме резултатите така, че сумата от трите вероятности да се добави към 1.

# Number of simulations
n_sim = 5000

# Track scenarios
oakland_stadium_money_probabilities_optimistic_list = []
oakland_stadium_money_probabilities_neutral_list = []
oakland_stadium_money_probabilities_pessimistic_list = []

oakland_ticket_sales_probabilities_optimistic_list = []
oakland_ticket_sales_probabilities_neutral_list = []
oakland_ticket_sales_probabilities_pessimistic_list = []

oakland_rsn_revenue_probabilities_optimistic_list = []
oakland_rsn_revenue_probabilities_neutral_list = []
oakland_rsn_revenue_probabilities_pessimistic_list = []

vegas_stadium_money_probabilities_optimistic_list = []
vegas_stadium_money_probabilities_neutral_list = []
vegas_stadium_money_probabilities_pessimistic_list = []

vegas_ticket_sales_probabilities_optimistic_list = []
vegas_ticket_sales_probabilities_neutral_list = []
vegas_ticket_sales_probabilities_pessimistic_list = []

vegas_rsn_revenue_probabilities_optimistic_list = []
vegas_rsn_revenue_probabilities_neutral_list = []
vegas_rsn_revenue_probabilities_pessimistic_list = []

oakland_EV_list = []
vegas_EV_list = []

decision_list = []

# Create data frame of all possible outcomes
decision_list = ['Oakland', 'Las Vegas']

# First Node
chance_node_stadium_money_scenarios = ['Optimistic', 'Neutral', 'Pessimistic']
chance_node_stadium_money_values = [855, 500, 0]

#Second Node
chance_node_ticket_sales_scenarios = ['Optimistic', 'Neutral', 'Pessimistic']
chance_node_ticket_sales_values_per_year = [80, 55, 27]

# Third Node
chance_node_rsn_revenue_scenarios = ['Optimistic', 'Neutral', 'Pessimistic']
chance_node_rsn_revenue_values_per_year = [60, 45, 36]

# Convert annual values to NPV of 10 year time horizon
time_horizon = 10 # years
discount_rate = 0.03 # per year
chance_node_ticket_sales_values = [val * (1 - (1/((1 + discount_rate)**time_horizon)))/discount_rate for val in chance_node_ticket_sales_values_per_year]
chance_node_rsn_revenue_values = [val * (1 - (1/((1 + discount_rate)**time_horizon)))/discount_rate for val in chance_node_rsn_revenue_values_per_year]


# Run the probabilistic sensitivity analysis n_sim times
for n in range(n_sim):
    
    ## Set up tree
    #First node
    chance_node_stadium_money_probabilities_oakland = np.random.uniform(0,1,3)
    chance_node_stadium_money_probabilities_oakland = chance_node_stadium_money_probabilities_oakland / np.sum(chance_node_stadium_money_probabilities_oakland)
    
    chance_node_stadium_money_probabilities_vegas = np.random.uniform(0,1,3)
    chance_node_stadium_money_probabilities_vegas = chance_node_stadium_money_probabilities_vegas / np.sum(chance_node_stadium_money_probabilities_vegas)

    #Second Node
    chance_node_ticket_sales_probabilities_oakland = np.random.uniform(0,1,3)
    chance_node_ticket_sales_probabilities_oakland = chance_node_ticket_sales_probabilities_oakland / np.sum(chance_node_ticket_sales_probabilities_oakland)
    
    chance_node_ticket_sales_probabilities_vegas = np.random.uniform(0,1,3)
    chance_node_ticket_sales_probabilities_vegas = chance_node_ticket_sales_probabilities_vegas / np.sum(chance_node_ticket_sales_probabilities_vegas)

    # Third Node
    chance_node_rsn_revenue_probabilities_oakland = np.random.uniform(0,1,3)
    chance_node_rsn_revenue_probabilities_oakland = chance_node_rsn_revenue_probabilities_oakland / np.sum(chance_node_rsn_revenue_probabilities_oakland)
    
    chance_node_rsn_revenue_probabilities_vegas = np.random.uniform(0,1,3)
    chance_node_rsn_revenue_probabilities_vegas = chance_node_rsn_revenue_probabilities_vegas / np.sum(chance_node_rsn_revenue_probabilities_vegas)
    
    # Evaluate Tree
    # Create data frame of all possible scenarios
    decision_list_list_for_df = []
    chance_node_stadium_money_list_for_df = []
    chance_node_stadium_money_probability_list_for_df = []
    chance_node_stadium_money_value_list_for_df = []
    chance_node_ticket_sales_list_for_df = []
    chance_node_ticket_sales_probability_list_for_df = []
    chance_node_ticket_sales_value_list_for_df = []
    chance_node_rsn_revenue_list_for_df = []
    chance_node_rsn_revenue_probability_list_for_df = []
    chance_node_rsn_revenue_value_list_for_df = []

    for i in decision_list:
        for j in range(len(chance_node_stadium_money_scenarios)):
            for k in range(len(chance_node_rsn_revenue_scenarios)):
                for m in range(len(chance_node_rsn_revenue_scenarios)):
                    decision_list_list_for_df.append(i)
                    chance_node_stadium_money_list_for_df.append(chance_node_stadium_money_scenarios[j])
                    chance_node_stadium_money_value_list_for_df.append(chance_node_stadium_money_values[j])
                    chance_node_ticket_sales_list_for_df.append(chance_node_ticket_sales_scenarios[k])
                    chance_node_ticket_sales_value_list_for_df.append(chance_node_ticket_sales_values[k])
                    chance_node_rsn_revenue_list_for_df.append(chance_node_rsn_revenue_scenarios[m])
                    chance_node_rsn_revenue_value_list_for_df.append(chance_node_rsn_revenue_values[m])

                    if i == 'Oakland':
                        chance_node_stadium_money_probability_list_for_df.append(chance_node_stadium_money_probabilities_oakland[j])
                        chance_node_ticket_sales_probability_list_for_df.append(chance_node_ticket_sales_probabilities_oakland[k])
                        chance_node_rsn_revenue_probability_list_for_df.append(chance_node_rsn_revenue_probabilities_oakland[m])
                    elif i == 'Las Vegas':
                        chance_node_stadium_money_probability_list_for_df.append(chance_node_stadium_money_probabilities_vegas[j])
                        chance_node_ticket_sales_probability_list_for_df.append(chance_node_ticket_sales_probabilities_vegas[k])
                        chance_node_rsn_revenue_probability_list_for_df.append(chance_node_rsn_revenue_probabilities_vegas[m])

    decision_tree_df = pd.DataFrame(list(zip(decision_list_list_for_df, chance_node_stadium_money_list_for_df,
                                             chance_node_stadium_money_probability_list_for_df,
                                             chance_node_stadium_money_value_list_for_df,
                                             chance_node_ticket_sales_list_for_df,
                                             chance_node_ticket_sales_probability_list_for_df,
                                             chance_node_ticket_sales_value_list_for_df,
                                             chance_node_rsn_revenue_list_for_df,
                                             chance_node_rsn_revenue_probability_list_for_df,
                                             chance_node_rsn_revenue_value_list_for_df)),
                                   columns = ['Decision',
                                              'Stadium_Money_Result', 'Stadium_Money_Prob', 'Stadium_Money_Value',
                                              'Ticket_Sales_Result', 'Ticket_Sales_Prob', 'Ticket_Sales_Value', 
                                              'RSN_Revenue_Result', 'RSN_Revenue_Prob', 'RSN_Revenue_Value'])
    decision_tree_df['RSN_EV'] = decision_tree_df['RSN_Revenue_Prob'] * decision_tree_df['RSN_Revenue_Value']

    # Consolidate the RSN_EV values
    RSN_rollback_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob', 'Ticket_Sales_Result', 'Ticket_Sales_Prob'])['RSN_EV'].sum().reset_index()

    # Keep the rest of the columns
    decision_tree_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob', 'Ticket_Sales_Result', 'Ticket_Sales_Prob'])['Stadium_Money_Value', 'Ticket_Sales_Value'].mean().reset_index()

    # merge two dataframes
    decision_tree_df = pd.merge(decision_tree_df, RSN_rollback_df, on = ['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob', 'Ticket_Sales_Result', 'Ticket_Sales_Prob'])

    decision_tree_df['Ticket_Sales_RSN_EV'] = decision_tree_df['Ticket_Sales_Prob'] * decision_tree_df['Ticket_Sales_Value'] + decision_tree_df['RSN_EV']

    # Consolidate the Ticket Sales and RSN_EV values
    ticket_sales_rollback_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob'])['Ticket_Sales_RSN_EV'].sum().reset_index()

    # Keep the rest of the columns
    decision_tree_df = decision_tree_df.groupby(['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob'])['Stadium_Money_Value'].mean().reset_index()

    # merge two dataframes
    decision_tree_df = pd.merge(decision_tree_df, ticket_sales_rollback_df, on = ['Decision', 'Stadium_Money_Result', 'Stadium_Money_Prob'])
    
    decision_tree_df['Stadium_Money_Ticket_Sales_RSN_EV'] = decision_tree_df['Stadium_Money_Prob'] * decision_tree_df['Stadium_Money_Value'] + decision_tree_df['Ticket_Sales_RSN_EV']

    # Consolidate the Stadium Money, Ticket Sales, and RSN_EV values
    decision_tree_df = decision_tree_df.groupby(['Decision'])['Stadium_Money_Ticket_Sales_RSN_EV'].sum().reset_index()
    
    # Fill out lists for meta-model inputs
    oakland_stadium_money_probabilities_optimistic_list.append(chance_node_stadium_money_probabilities_oakland[0])
    oakland_stadium_money_probabilities_neutral_list.append(chance_node_stadium_money_probabilities_oakland[1])
    oakland_stadium_money_probabilities_pessimistic_list.append(chance_node_stadium_money_probabilities_oakland[2])

    oakland_ticket_sales_probabilities_optimistic_list.append(chance_node_ticket_sales_probabilities_oakland[0])
    oakland_ticket_sales_probabilities_neutral_list.append(chance_node_ticket_sales_probabilities_oakland[1])
    oakland_ticket_sales_probabilities_pessimistic_list.append(chance_node_ticket_sales_probabilities_oakland[2])

    oakland_rsn_revenue_probabilities_optimistic_list.append(chance_node_rsn_revenue_probabilities_oakland[0])
    oakland_rsn_revenue_probabilities_neutral_list.append(chance_node_rsn_revenue_probabilities_oakland[1])
    oakland_rsn_revenue_probabilities_pessimistic_list.append(chance_node_rsn_revenue_probabilities_oakland[2])

    vegas_stadium_money_probabilities_optimistic_list.append(chance_node_stadium_money_probabilities_vegas[0])
    vegas_stadium_money_probabilities_neutral_list.append(chance_node_stadium_money_probabilities_vegas[1])
    vegas_stadium_money_probabilities_pessimistic_list.append(chance_node_stadium_money_probabilities_vegas[2])

    vegas_ticket_sales_probabilities_optimistic_list.append(chance_node_ticket_sales_probabilities_vegas[0])
    vegas_ticket_sales_probabilities_neutral_list.append(chance_node_ticket_sales_probabilities_vegas[1])
    vegas_ticket_sales_probabilities_pessimistic_list.append(chance_node_ticket_sales_probabilities_vegas[2])

    vegas_rsn_revenue_probabilities_optimistic_list.append(chance_node_rsn_revenue_probabilities_vegas[0])
    vegas_rsn_revenue_probabilities_neutral_list.append(chance_node_rsn_revenue_probabilities_vegas[1])
    vegas_rsn_revenue_probabilities_pessimistic_list.append(chance_node_rsn_revenue_probabilities_vegas[2])

    oakland_EV_list.append(decision_tree_df['Stadium_Money_Ticket_Sales_RSN_EV'][0])
    vegas_EV_list.append(decision_tree_df['Stadium_Money_Ticket_Sales_RSN_EV'][1])
    
    print(n)

Сега можем да поставим резултатите в нова рамка от данни, която можем да използваме, за да обучим нашия модел за машинно обучение:

decision_tree_psa_data_df = pd.DataFrame(list(zip(oakland_stadium_money_probabilities_optimistic_list, 
                                         oakland_stadium_money_probabilities_neutral_list,
                                         oakland_stadium_money_probabilities_pessimistic_list,
                                         oakland_ticket_sales_probabilities_optimistic_list,
                                         oakland_ticket_sales_probabilities_neutral_list,
                                         oakland_ticket_sales_probabilities_pessimistic_list,
                                         oakland_rsn_revenue_probabilities_optimistic_list,
                                         oakland_rsn_revenue_probabilities_neutral_list,
                                         oakland_rsn_revenue_probabilities_pessimistic_list,
                                         vegas_stadium_money_probabilities_optimistic_list,
                                         vegas_stadium_money_probabilities_neutral_list, 
                                         vegas_stadium_money_probabilities_pessimistic_list,
                                         vegas_ticket_sales_probabilities_optimistic_list,
                                         vegas_ticket_sales_probabilities_neutral_list,
                                         vegas_ticket_sales_probabilities_pessimistic_list,
                                         vegas_rsn_revenue_probabilities_optimistic_list,
                                         vegas_rsn_revenue_probabilities_neutral_list, 
                                         vegas_rsn_revenue_probabilities_pessimistic_list,
                                         oakland_EV_list, vegas_EV_list)),
                                   columns = ['oakland_stad_mon_prob_optimistic',
                                              'oakland_stad_mon_prob_neutral',
                                              'oakland_stad_mon_prob_pessimistic',
                                              'oakland_ticket_sales_prob_optimistic',
                                              'oakland_ticket_sales_prob_neutral',
                                              'oakland_ticket_sales_prob_pessimistic',
                                              'oakland_rsn_rev_prob_optimistic',
                                              'oakland_rsn_rev_prob_neutral',
                                              'oakland_rsn_rev_prob_pessimistic',
                                              'vegas_stad_mon_prob_optimistic',
                                              'vegas_stad_mon_prob_neutral',
                                              'vegas_stad_mon_prob_pessimistic',
                                              'vegas_ticket_sales_prob_optimistic',
                                              'vegas_ticket_sales_prob_neutral',
                                              'vegas_ticket_sales_prob_pessimistic',
                                              'vegas_rsn_rev_prob_optimistic',
                                              'vegas_rsn_rev_prob_neutral',
                                              'vegas_rsn_rev_prob_pessimistic',
                                              'oakland_EV', 'vegas_EV'])

# Add decision based on EV
decision_tree_psa_data_df['decision'] = 'Oakland'
decision_tree_psa_data_df.loc[decision_tree_psa_data_df['vegas_EV'] > decision_tree_psa_data_df['oakland_EV'],'decision'] = 'Las Vegas'

Сега ще обучим основно дърво на решения за машинно обучение, като използваме sci-kit learn package. Тъй като входните данни са вероятности между 0 и 1 и ние използваме модел, базиран на дърво, няма да се налага да правим каквото и да е мащабиране на функции или инженерство. За целите на визуализацията на блога ограничих дървото до дълбочина 3. Въпреки това, колкото по-голяма е дълбочината на дървото, толкова по-вероятно е да постигнете по-голяма точност.

from sklearn.datasets import load_iris
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import train_test_split
from sklearn import tree

#Features
X = decision_tree_psa_data_df.drop(['oakland_EV', 'vegas_EV', 'decision'], axis = 1)
#labels
y = decision_tree_psa_data_df['decision']

# split into train (70%) and test set (30%)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=32)

# Create decision tree model with maximum depth of 3 to keep recommendation managable
dec_tree_model = tree.DecisionTreeClassifier(random_state=32, max_depth = 3, class_weight = 'balanced')
dec_tree_model = dec_tree_model.fit(X_train, y_train)

Нашият модел завърши с приличен, но не перфектен AUC от почти 0,8. (AUC е начин за измерване на точността на модела въз основа на истинските и фалшивите положителни проценти. За повече относно мерките за точност на модела вижте предишния ми блог за оценка на точността на прогнозираните резултати на ESPN фентъзи футбол „тук“.) Това е достатъчно уважавано за да продължим с упражнението. Разбира се, има няколко начина да направите класификатора на дървото на решенията по-точен, включително увеличаване на максималната дълбочина, настройка на хиперпараметри или провеждане на повече симулации за увеличаване на количеството данни.

from sklearn.metrics import roc_auc_score
roc_auc_score(y_test, dec_tree_model.predict_proba(X_test)[:, 1])

Сега, след като сме доволни от представянето, можем да разгледаме визуално дървото на решенията за влака. Всяко разделение в дървото представлява друго измерение на набор от бизнес правила. Във всяко поле (или лист) на отпечатаното дърво първият ред ще представлява правилото, използвано от модела за разделяне на данните, вторият ред е индексът на Джини, който описва разпределението на класовете в листа (където 0,5 представлява равно число на всеки клас и 0 или 1 представлява само един клас), третият ред показва броя на пробите от всеки клас, а четвъртият ред показва етикета, който моделът присвоява на всички проби в този лист. Можем да отпечатаме полученото дърво по-долу:

# Plot decision tree results to see how decisions were made
import matplotlib.pyplot as plt
fig = plt.figure(figsize = (14,14))
tree.plot_tree(dec_tree_model, filled = True, feature_names = X.columns, fontsize = 8, class_names = ['Las Vegas', 'Oakland'])
plt.show()

От дървото на решенията ни за машинно обучение можем да видим, че класификацията дали A's трябва да останат в Оукланд или да се преместят в Лас Вегас се основава първо на вероятността за оптимистични приходи от RSN и след това на вероятностите, свързани с продажбите на билети в Оукланд.

Лас Вегас вероятно е предпочитаната дестинация, когато:

  • Вероятността за оптимистични приходи от RSN в Лас Вегас е по-голяма от 0,4 (освен когато вероятността за оптимистични приходи от RSN в Оукланд е по-голяма от 0,341 И вероятността за оптимистични продажби на билети в Оукланд е по-голяма от 0,355)
  • ИЛИ вероятността за оптимистични приходи от RSN в Оукланд е по-малка или равна на 0,468 И вероятността за оптимистични продажби на билети в Лас Вегас е по-голяма от 0,438.

Интересното е, че въпреки цялото медийно бърборене за публично или частно финансиране за нов стадион, нашият модел сочи приходите на RSN и продажбите на билети. Разликата може да се дължи на нашия 10-годишен времеви хоризонт или може да се дължи на организацията, която търси оправдание, одобрено от MLB, за да напусне Оукланд. Така или иначе,този подход подчертава важно прозрение, което екипът за наука за данни може да предостави на вземащите решения, за да информира бизнес стратегията. Методи като този могат да отведат вашия модел от интересно теоретично упражнение до промяна на мненията в C-suite.

Как да валидираме модела за машинно обучение?

Като се има предвид, че се опитваме да информираме за изключително важно решение, важно е да се уверим, че нашият модел е устойчив на разлики във входните данни или небалансиран класов набор от етикети. За да отчетем последното, ще забележите, ние включихме class_weight = ‘balanced’ в създаването на нашия модел за машинно обучение. За да отчетем първото и за валидиране на модела, можем да използваме резултата за кръстосано валидиране, за да видим какви други показатели за ефективност на разделяне на обучение/тест биха били:

# 10-fold cross-validation scores
cross_val_score(dec_tree_model, X, y, cv=10)

Резултатът е следният: array([0.724, 0.722, 0.718, 0.72, 0.722, 0.708, 0.732, 0.726, 0.76, 0.702]), което ни казва, че при 10 различни възможни разделяния на влак/тест нашият модел е имал сходна производителност.

Какво научихме?

С това преминахме от бизнес въпрос за преместването на бейзболния отбор на A до връщане назад на модел на дърво на решенията за анализ на решения, за да разкрием защо A може да се насочи към Лас Вегас до използване на дърво на решения за машинно обучение, за да обобщим нашите резултати в смилаеми бизнес правила, които ръководството може да използва, за да реши дали да се премести или не. Надяваме се, че ще можете да използвате подобна методология или подход, за да информирате вземащите решения във вашата собствена организация или в ежедневието си.

Референции

[1] Sutelan, E, График за преместване на Athletics Las Vegas: Стадион се спъва, неуспехи във финансирането по пътя към заминаването на A от Оукланд (2023), The Sporting News

[2] Kenton, W, Анализ на решенията (DA): Дефиниция, употреби и примери (2022), Investopedia

[3] Хауърд, Р. и Абас, А, Основи на анализа на решенията (2014 г.)

[4] Morss, E., Финанси на бейзболната лига на Висшата лига: какво ни казват числата (2019), Morss Global Finance

[5] Гриър, Дж., Защо Raiders се преместиха в Лас Вегас? Обяснявайки преместването на франчайза през 2020 г. от Оукланд към Града на греха (2020), The Sporting News

[6] Андре, Д. Доклад: Raiders първи през 2021 г. приходи от билети за NFL (2022), Fox 5 Las Vegas

[7] Malloy, G. and Brandeau, M. Кога масовата профилактика е рентабилна за контрол на епидемията? Сравнение на подходите за вземане на решения (2022), Вземане на медицински решения

Интересувате ли се от моето съдържание? Моля, обмислете „последвайте ме в Medium“.

Целият код и данни могат да бъдат намерени в GitHub тук: gspmalloy/oakland_as_decision_trees: Код за моя блог „Анализ на решения и дървета в Python — Случаят на Оукланд А“ (github.com)

Последвайте ме в Twitter: @malloy_giovanni

Мислите ли, че А трябва да останат в Оукланд? Да се ​​преместя ли във Вегас? Може би опитайте в друг град? Какъв е вашият опит с използването на машинно обучение за мета-моделиране? Ще се радвам да чуя вашите мисли! Поддържайте дискусията, като коментирате.