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

Введение

Прошлой осенью я написал статью под названием: Rfviz: интерактивный пакет визуализации для интерпретации случайных лесов в R. Эта статья представляет собой руководство по инструменту визуализации в R. Я хотел дать возможность не использовать инструмент визуализации для получения результатов того же типа. Итак, эта статья представляет собой пример написания кода только для использования оценок локальной важности из библиотеки randomForest в R. Мы будем использовать ее для определения общей важности переменных на уровне классов. Используя этот метод, мы будем изучать, почему некоторые игроки MLB попадают в Зал славы, а многие не попадают в Зал славы.

Теоретические основы

Случайные леса

Случайные леса (Breiman (2001)) соответствуют количеству деревьев (обычно 500 или более) для данных регрессии или классификации. Каждое дерево соответствует начальной выборке данных, поэтому некоторые наблюдения не включаются в соответствие каждого дерева (они называются наблюдениями из коробки для дерева). Независимо в каждом узле каждого дерева случайным образом выбирается относительно небольшое количество переменных-предикторов (называемых mtry), и эти переменные используются для поиска наилучшего разделения. Деревья выращены глубоко и не обрезаны. Чтобы предсказать новое наблюдение, наблюдение передается по всем деревьям, а прогнозы усредняются (регрессия) или голосуются (классификация).

Важность переменной

Оценка локальной важности получается для каждого наблюдения в наборе данных для каждой переменной. Чтобы получить локальную оценку важности для наблюдения i и переменной j, случайным образом переставьте переменную j для каждого дерева, в котором наблюдение i не в порядке, и сравните ошибку для переменной-jперестановки данных с фактической ошибкой. Средняя разница в ошибках по всем деревьям, для которых наблюдение i не соответствует действительности, является его локальной оценкой важности.

(Общая) оценка важности переменной для переменной j — это среднее значение ее локальной важности по всем наблюдениям.

Пример

Наши данные — это статистика Высшей лиги бейсбола. Вопрос, который мы попытаемся помочь объяснить, заключается в том, почему некоторые игроки MLB попадают в Зал славы, а многие не попадают в Зал славы.

Мы будем использовать статистику отбивания и подачи, а также награды по итогам сезона для этих игроков. В этом примере мы игнорируем статистику подачи и награды питчерам в конце сезона. Используя Random Forests в R, мы обнаружим на основе классов общую важность переменных и то, как мы можем использовать результаты, чтобы помочь ответить на вопрос.

В этом примере используются данные из пакета R Lahman, который представляет собой набор данных статистики бейсбола, который содержит статистику подачи, ударов, выставления на поле и наград для Высшей лиги бейсбола с 1871 по 2019 год (Lahman 2020). Он находится под лицензией Creative Commons Attribution-ShareAlike 3.0 Unported License.

Для справки об определениях переменных из тех, которые мы использовали для агрегирования и создания нашего набора данных, перейдите по этой ссылке: http://www.seanlahman.com/files/database/readme2017.txt.

Подготовка и исследование данных

library('Lahman')
library(randomForest)
library(dplyr)
#Get the list of players inducted into the Hall of Fame
HallOfFamers <-
  HallOfFame %>%
  group_by(playerID) %>%
  filter((votedBy=="BBWAA" | votedBy=="Special Election") 
         & category == "Player") %>%
  summarise(inducted = sum(inducted == "Y")) %>% 
  ungroup()
#Batting Statistics
Batting_Stats <-
  Batting %>%
  group_by(playerID) %>%
  mutate(total_BA=sum(H)/sum(AB),
            total_AB=sum(AB), total_R=sum(R),
            total_X2B=sum(X2B),total_X3B=sum(X3B), 
            total_H = sum(H), total_HR = sum(HR),
            total_RBI=sum(RBI), total_SB=sum(SB), total_CS=sum(CS),
            total_BB=sum(BB), total_SO=sum(SO), 
            total_IBB=sum(IBB), total_HBP=sum(HBP),
            total_SH=sum(SH), total_SF=sum(SF),
            total_GIDP=sum(GIDP)) %>%
  select(playerID, total_BA, total_AB, total_R, total_X2B, total_X3B, total_H, total_HR,
         total_RBI, total_SB, total_CS, total_BB, total_SO, total_IBB, total_HBP, total_SH,
         total_SF, total_GIDP) %>% 
  group_by_all() %>% 
  summarise() %>% 
  arrange(desc(total_H)) %>% 
  ungroup()
#Fielding Statistics
Fielding_Stats <- 
  Fielding %>% group_by(playerID) %>% 
  mutate(POS=toString(unique(POS)), total_G=sum(G), total_GS=sum(GS), total_InnOuts=sum(InnOuts),
            total_PO=sum(PO), total_A=sum(A), total_E=sum(E), total_DP=sum(DP), total_PB_Catchers=sum(PB),
            total_WP_Catchers=sum(WP), total_SB_Catchers=sum(SB), total_CS_Catchers=sum(CS), mean_ZR=mean(ZR)) %>% 
  select(playerID, POS, total_G, total_GS, total_InnOuts, total_PO, total_A, total_E, total_DP,
         total_PB_Catchers, total_WP_Catchers, total_SB_Catchers, total_CS_Catchers, mean_ZR) %>% 
  group_by_all() %>% 
  summarise() %>% 
  arrange(desc(total_G)) %>% 
  ungroup()
#End of Season Awards
Season_Awards <-
  AwardsPlayers %>%
  group_by(playerID) %>%
  mutate(Count_All_Star=sum(awardID=='Baseball Magazine All-Star' | awardID=='TSN All-Star'), Count_MVP = sum(awardID == "Most Valuable Player"), 
         Count_Silver_Slugger = sum(awardID == "Silver Slugger"), Count_Gold_Glove = sum(awardID == "Gold Glove")) %>% 
  select(playerID, Count_All_Star, Count_MVP, Count_Silver_Slugger, Count_Gold_Glove) %>% 
  group_by_all() %>% 
  summarise() %>% 
  ungroup()
#Joining the datasets together
HOF_Data <- Batting_Stats %>% 
  full_join(Fielding_Stats, by=c('playerID')) %>% 
  left_join(Season_Awards, by='playerID') %>% 
  left_join(HallOfFamers, by='playerID')  %>%  
  #Filling in NA's based on data type
  mutate_if(is.integer, ~replace(., is.na(.), 0)) %>% 
  mutate_if(is.numeric, ~replace(., is.na(.), 0)) %>% 
  mutate_if(is.character, ~replace(., is.na(.), 'xx')) %>% 
  mutate_if(is.character, as.factor) #Converting characters to factors in preparation for Random Forests
#Double checking filling in NA's and converting the response variable to a factor. 
HOF_Data[is.na(HOF_Data)] <- 0
HOF_Data$inducted <- as.factor(HOF_Data$inducted)
#Looking at the spread of inducted vs. not to the Hall of Fame
table(HOF_Data$inducted)

Этот результат ниже показывает, что в нашем наборе данных есть 19 773 игрока, которые не были введены в Зал славы, и 125, которые:

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

Вывод модели

#For purposes of exploratory analysis, we will not be splitting into training and test.
#Quick trick for getting all variables separated by a '+' for the formula. (We just omit the response variable and any others we don't want before copying as pasting the output) 
paste(names(HOF_Data), collapse='+')
rf <- randomForest(inducted~total_BA+total_AB+total_R+total_X2B+total_X3B+
        total_H+total_HR+total_RBI+total_SB+total_CS+total_BB+
        total_SO+total_IBB+total_HBP+total_SH+total_SF+total_GIDP+
        total_G+total_GS+total_InnOuts+total_PO+total_A+total_E+
        total_DP+total_PB_Catchers+total_WP_Catchers+
        total_SB_Catchers+total_CS_Catchers+mean_ZR+Count_All_Star+
        Count_MVP+Count_Silver_Slugger+Count_Gold_Glove,
                   data=HOF_Data, 
                   localImp=TRUE, cutoff=c(.8,.2))
rf

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

Общая важность переменной для класса

Далее, давайте посмотрим на то, что, согласно оценкам местной важности, является наиболее важным для классификации тех, кто попал в Зал славы.

#Rather than using the model for predictions I am using it to see how the trees separated the data. This way it is more of an unsupervised learning problem. Because of this I am okay predicting on the data set I used to train the model.
HOF_Data$predicted <- predict(rf, HOF_Data)
#Looking at the class-wise variable importance for those who were classified as making the Hall of Fame.
HF <- data.frame(t(rf$localImportance[,which(HOF_Data$predicted==1)]))
sort((apply((HF),2,mean)), decreasing=TRUE)

Сортировка ((применить ((HF), 2, среднее)), уменьшение = ИСТИНА) вычисляет общую важность переменной для класса 1 или тех игроков, которые модель классифицировала как попадающих в Зал славы. Это потому, что мы создали подмножество для этого класса на предыдущем этапе кодирования. Теперь у нас есть общая важность переменных на уровне классов.

Теперь давайте посмотрим на противоположный класс

#Looking at the class-wise variable importance for those who were classified as not making the Hall of Fame.
NHF <- data.frame(t(rf$localImportance[,which(HOF_Data$predicted==0)]))
sort((apply((NHF),2,mean)), decreasing=TRUE)

Мы видим, что классы имеют разные переменные в зависимости от того, что наиболее важно для предсказания этого класса. Но каковы эти различия? Давайте углубимся в это для тех, кто входит в Зал славы.

Ответы на вопрос "Почему"

Согласно важным локальным оценкам от Random Forests, 5 основных характеристик для включения в Зал славы отбивающих / полевых игроков:

  1. Count_All_Star (сколько раз игрок был назван All-Star)
  2. total_G (сколько всего игр сыграл игрок)
  3. total_SH (сколько всего жертвоприношений попадает в цель игрока)
  4. total_RBI (сколько всего пробежек забил игрок)
  5. total_HR (сколько всего хоумранов сделал игрок)

Давайте возьмем три из них, Count_All_Star, total_HR и total_G, и сравним данные между двумя классами:

summary((HOF_Data[HOF_Data$predicted==1,'Count_All_Star']))
summary((HOF_Data[HOF_Data$predicted==0,'Count_All_Star']))

summary((HOF_Data[HOF_Data$predicted==1,’total_HR’]))
summary((HOF_Data[HOF_Data$predicted==0,’total_HR’]))

summary((HOF_Data[HOF_Data$predicted==1,’total_G’]))
summary((HOF_Data[HOF_Data$predicted==0,’total_G’]))

Мы можем видеть, что локальные оценки важности случайных лесов говорят о том, что большее количество лет участия в Матче звезд, большее количество хоум-ранов и большее количество сыгранных игр — это 3 из 5 наиболее важных переменных для того, чтобы попасть в Зал славы. Слава отбивающего / полевого игрока. Используя этот метод, мы можем довольно быстро увидеть фактические разбросы данных и различия между классами.

Примечание. Глядя на эти очевидные исключения, игрок, сыгравший 3528 игр, — это Чарли Герих, который фактически попал в Зал славы. Он является неправильной классификацией. Игрок, который совершил 762 хоумрана и не попал в Зал славы, — это Барри Бондс, известный пользователь стероидов. Пит Роуз, сыгравший 3528 игр и не попавший в Зал, был исключен из-за ставок на игры. Довольно иронично, что последние два не попали в Зал славы и что Random Forests классифицировал их как не попавших в него.

Заключение

В заключение, кто-то, кто имеет средний уровень знаний о бейсболе, может знать, что создание клуба 500 Home Run и 10-кратное участие в Матче звезд дает вам неплохие шансы на попадание в Зал славы. Однако это не гарантирует. Похоже, это так, потому что нет четких правил создания Зала славы. С другой стороны, что, если кто-то ничего не знает о Высшей лиге бейсбола? Или тема, по которой они работают с моделью? Что ж, используя локальные оценки важности и общую важность переменных по классам со случайными лесами в R, они могут помочь объяснить причину своей проблемы классификации.

Ссылки:

Брейман, Л. 2001. Случайные леса. Машинное обучение. http://www.springerlink.com/index/u0p06167n6173512.pdf.

Брейман, Л., и А. Катлер. 2004. Случайные леса. https://www.stat.berkeley.edu/~breiman/RandomForests/cc_graphics.htm.

C Beckett, Rfviz: пакет интерактивной визуализации для случайных лесов в R, 2018 г., https://chrisbeckett8.github.io/Rfviz.

Лахман, С. (2020) База данных бейсбола Лахмана, 1871–2019 гг., Главная страница, http://www.seanlahman.com/baseball-archive/statistics/

Плескофф, Берни. Каковы стандарты для избрания в Национальный зал бейсбольной славы? Forbes, журнал Forbes, 22 января 2020 г., https://www.forbes.com/sites/berniepleskoff/2020/01/21/what-are-the-standards-for-election- в-национальный-зал-бейсбольной-славы/?sh=17c57173149e.