Прогнозирование выживших на Титанике с помощью линейного программирования

Соревнование Titanic на Kaggle — известная тема для старта соревнования по машинному обучению (ML) и знакомства с миром анализа данных и моделирования. Имея некоторые навыки работы с Python (или другими языками программирования) и использование некоторых моделей, вы можете начать участвовать.

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

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

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

Что такое линейное программирование?

Линейное программирование имеет тенденцию оптимизировать проблему, применяя ограничения. Для тех, кто проводит время за судоку, это возможность заполнить сетку числами (от 1 до 9) и соблюдать некоторые правила, такие как:

  • только одно число в строках и столбцах
  • заполнить сетку 3x3 числами от 1 до 9

Эти 2 пункта можно рассматривать как «ограничения». Оптимизация может заключаться во времени, потраченном на решение судоку, или в количестве проб и ошибок.

В этой статье я работал с pulse, очень хорошей и простой в использовании библиотекой Python. Более подробную информацию вы можете найти здесь. Есть и другие библиотеки, такие как PyOmo или даже SkLearn, предлагающие некоторые методы.

Титаник Конкурс

Соревнование Titanic Kaggle

Цель конкурса — предсказать выжившего (и погибшего пассажира) по невидимому набору данных, содержащему 418 наблюдений (данные о пассажирах). Для этого нам нужно обучить модель с набором данных из 891 наблюдения, для которых мы знаем цель (выжили = 1 или умерли = 0). Наблюдения представляют собой список характеристик для каждого пассажира, таких как возраст, класс, место посадки пассажира, член семьи или нет, пол и некоторые другие детали.

Описание набора данных

Обучающая выборка для обучения модели содержит 891 наблюдение. 342 выжили. Мы знаем, читая Википедию, что на лодке было 1316 пассажиров и 37,8% выжили. Это означает, что 498 пассажиров выжили. Невидимый набор данных содержит 498–342 (146) выживших пассажиров. Мы должны найти 146 пассажиров из этого невидимого набора данных, которые выжили.

Интернет – это кладезь информации, связанной с этой трагедией. Вы можете найти соотношение пассажиров по классам, которые выжили, как не могли попасть на борт и даже имена пассажиров, которые выжили.

Из набора обучающих данных мы знаем, что выжили 136 пассажиров 1-го класса, 87 пассажиров 2-го класса и 119 пассажиров 3-го класса.

Интернет сообщает нам, что 61% (202) пассажиров 1-го класса выжили, 42% (118) - 2-го класса и 24% (178) - 3-го класса. Итак, мы должны найти в невидимом наборе данных:

  • 202–136 пассажиров 1-го класса = 66
  • 118–87 пассажиров 2-го класса = 31
  • 178–119 пассажиров 3-го класса = 59

У нас есть 3 ограничения, чтобы начать оптимизацию задачи :-)

Линейное программирование для решения проблемы

С помощью нескольких строк кода мы можем легко реализовать эту задачу и попросить «pulp» решить ее!

Давайте запустим Блокнот, импортировав библиотеки.

import pyforest
from pulp import *

Затем мы можем загрузить набор данных, который мы можем найти на веб-сайте Kaggle.

train = pd.read_csv('titanic/train.csv')
test = pd.read_csv('titanic/test.csv')
test.shape,train.shape

Подготовьте набор данных для LP

# create a copy of test set with Pclass
cols = ['PassengerId', 'Pclass','Sex']
df = test[cols].copy()
df = df.astype({"Pclass": 'str'})
# set passenger Id as index
df.set_index('PassengerId', inplace=True)
df['Passenger']=df.index
df = df.astype({"Passenger": 'str'})
df = pd.get_dummies(df)
#df.columns = ['Pclass1','Pclass2','Pclass3']
# add a column with constant 1 which will be used to be sure 
# passenger is choose only once (or none)
df['Selected']=1
# add a column which contains a value to be used for optimisation
# problem;
# value will be computed to maximise the score later
df['Score']=1
df.head()

Определите функцию для решателя

def setup_problem(df,sex=False, sex_class=False):

# Линейное программирование

prob = LpProblem("titanic_canot",LpMaximize)
passengers_items = list(df.index)
passengers_items
# Create a dictinary of unique for all passengers
unique = dict(zip(passengers_items,df['Score']))
class1 = dict(zip(passengers_items,df['Pclass_1']))
class2 = dict(zip(passengers_items,df['Pclass_2']))
class3 = dict(zip(passengers_items,df['Pclass_3']))
passengers_vars = LpVariable.dicts("PassengerId",passengers_items,lowBound=0,cat='Integer')
prob += lpSum([unique[i]*passengers_vars[i] for i in passengers_items])

Начните писать ограничения. Поскольку мне нужно точное количество пассажиров по классам, я определил ограничения по 2 значениям на класс {›= x и ‹= y}. Оба имеют одинаковое значение.

prob += lpSum([class1[f] * passengers_vars[f] for f in passengers_items]) >= 66
prob += lpSum([class1[f] * passengers_vars[f] for f in passengers_items]) <= 66
prob += lpSum([class2[f] * passengers_vars[f] for f in passengers_items]) >= 31
prob += lpSum([class2[f] * passengers_vars[f] for f in passengers_items]) <= 31
prob += lpSum([class3[f] * passengers_vars[f] for f in passengers_items]) >= 59
prob += lpSum([class3[f] * passengers_vars[f] for f in passengers_items]) <= 59

Я добавил специальное ограничение, чтобы убедиться, что пассажир выбран только один раз (или нет).

peoples = []
#numbers=["{0:04}".format(i) for i in range(892,1310)]
for i in range(892,1310):
key = 'Passenger_' + str(i)
temp = dict(zip(passengers_items,df[key]))
peoples.append(temp)
for people in peoples:
prob += lpSum([people[f] * passengers_vars[f] for f in passengers_items]) >= 0
prob += lpSum([people[f] * passengers_vars[f] for f in passengers_items]) <= 1
return prob

Вызов решателя

prob = setup_problem(df,False)
prob.solve()
print("Status:", LpStatus[prob.status])
to_remove = []
for v in prob.variables():
if v.varValue>0:
#print(v.name.replace('PassengerId_',''), "=", v.varValue)
to_remove.append(int(v.name.replace('PassengerId_','')))

Создать отправку для Kaggle

# create a submission dataset
submission = test[['PassengerId']].copy()
submission = submission.set_index('PassengerId')
submission.loc[to_remove,'Survived']=1
submission.fillna(0, inplace=True)
submission = submission.astype({"Survived": 'int'})
submission.reset_index().to_csv('submission-lp-002.csv', index=False)
submission.reset_index().head()

Посмотрите разбивку нашего представления

test_df = submission.merge(test, left_on='PassengerId', right_on='PassengerId')
test_df.query('Survived ==1')[['PassengerId','Pclass','Sex']].groupby(['Pclass','Sex']).count()

Оценка и другие ограничения

Эта первая отправка вернула оценку 58%, что не так уж плохо с несколькими строками кода и всего 3 ограничениями.

Сделайте еще одну попытку, добавив ограничение на секс.

Я обновил метод задачи, добавив ограничение, чтобы было не менее 85 женщин и не менее 30 мужчин. Это означает, что 115 передает 156, которые должны быть выбраны в невидимом наборе данных. После запуска задачи и отправки результата в Kaggle оценка подскочила до 68%.

if sex:
sex_female = dict(zip(passengers_items,df['Sex_female']))
sex_male = dict(zip(passengers_items,df['Sex_male']))
prob += lpSum([sex_female[f] * passengers_vars[f] for f in passengers_items]) >= 85
prob += lpSum([sex_male[f] * passengers_vars[f] for f in passengers_items]) >= 30

Заключение

Используя линейное программирование с ограничениями, мы приближаемся к 70%, просто устанавливая ограничения на количество пассажиров по классам для экономии и определяя разбивку по женщинам и мужчинам. Мы можем добавить еще много ограничений для оптимизации оценки, например:

  • возраст (взрослый или детский)
  • семейная группа (на основе функции SibSp или Parch)
  • каюта (в зависимости от расположения канота)

В следующем посте я покажу, как я пытался сесть на канот с ограничением, основываясь на описании, которое мы можем найти в Интернете о том, как садились на каноты.

«Титаник» стал настоящей трагедией и, к сожалению, хорошей темой для практиков машинного обучения.

Эта история была вдохновлена ​​этой статьей.