Предскажете оцелелите от Титаник с линейно програмиране

Състезанието Titanic на Kaggle е известна тема за започване на състезание за машинно обучение (ML) и за наслада от света на анализа и моделирането на данни. С няколко умения за Python (или други езици за програмиране) и използване на някои модели можете да започнете да участвате.

Завърших EPFL Extension School по програма за машинно обучение. По време на курсовете участвах в състезанието на Титаник, за да тренирам уменията си и, честно казано, този проблем с класификацията е много интересен.

Изпратих няколко комплекта, всички базирани на класификационни модели като дърво на решенията, произволна гора или линейна класификация. Изпратих и някакъв набор, предсказан с модел на PyCaret.

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

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

Линейното програмиране има тенденция да оптимизира проблем чрез прилагане на ограничения. За тези, които прекарват време в судоку, това е способността да попълват мрежа с числа (1 до 9) и да спазват някои правила като:

  • само едно число на редовете и колоните
  • попълнете мрежа от 3x3 с числа от 1 до 9

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

За тази статия работих с pulp, която е много добра и лесна за използване библиотека на Python. Можете да намерите повече информация тук. Има още други библиотеки като PyOmo или дори SkLearn предлага някои методи.

Състезание Титаник

Състезанието Titanic Kaggle

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

Описание на набора от данни

Учебният комплект за обучение на модела съдържа 891 наблюдения. 342 са оцелели. Ние знаем, като четем Wikipedia, че 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)
  • кабина (в зависимост от местоположението на каната)

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

Титаник беше истинска трагедия и за съжаление хубава тема за практикуващите машинно обучение.

Тази история е вдъхновена от тази статия.