Titanic data

Dane mozna pobrać po utworzeniu (darmowego) konta na portalu Kaggle.

Pobierz dane: interesują nas tylko pliki zbiorów train.csv i test.csv.

Zobaczmy jak wyglądają nasze dane:

import pandas as pd

train = pd.read_csv('../data/train.csv')
test = pd.read_csv('../data/test.csv')

print("train ma {} wierszy i {} kolumn".format(*train.shape))
print("test ma {} wierszy i {} kolumn".format(*test.shape))

print(f"train to obiekt typu {type(train)}")
train ma 891 wierszy i 12 kolumn
test ma 418 wierszy i 11 kolumn
train to obiekt typu <class 'pandas.core.frame.DataFrame'>
train.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   PassengerId  891 non-null    int64  
 1   Survived     891 non-null    int64  
 2   Pclass       891 non-null    int64  
 3   Name         891 non-null    object 
 4   Sex          891 non-null    object 
 5   Age          714 non-null    float64
 6   SibSp        891 non-null    int64  
 7   Parch        891 non-null    int64  
 8   Ticket       891 non-null    object 
 9   Fare         891 non-null    float64
 10  Cabin        204 non-null    object 
 11  Embarked     889 non-null    object 
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB

Metoda info() zwraca informacje o: - nazyach kolumn, - ich indeksy, - liczbę niepustych (null) elementów dla kazdej kolumny,
- typy danych.

test.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 418 entries, 0 to 417
Data columns (total 11 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   PassengerId  418 non-null    int64  
 1   Pclass       418 non-null    int64  
 2   Name         418 non-null    object 
 3   Sex          418 non-null    object 
 4   Age          332 non-null    float64
 5   SibSp        418 non-null    int64  
 6   Parch        418 non-null    int64  
 7   Ticket       418 non-null    object 
 8   Fare         417 non-null    float64
 9   Cabin        91 non-null     object 
 10  Embarked     418 non-null    object 
dtypes: float64(2), int64(4), object(5)
memory usage: 36.0+ KB

Dla zbioru testowego mamy jedną kolumnę (Survived) mniej, dlaczego?

Ze względu, iz nie planujemy wrzucać wyników modeli na kaggle zbiór test nie jest nam potrzebny.

Informacje z metody info() przedstawiają tylko ogólne rzeczy, zobaczmy jak zbiór train wygląda w środku.

train.head()
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

Kazda kolumna reprezentuje jedną zmienną naszych danych. Identyfikatorem, bądź kluczem naszej tabeli jest PassengerId, która przyjmuje rózną wartość dla kazdego wiersza. Czy taka zmienna moze być dobra do modelowania? Zmienna Survived realizuje zmienną celu naszego zadania - pasazer przezyl (1) lub nie (0). Pclass to zmienna opisująca klasę pokładu zgodnie z biletem.

Czyszczenie danych

Nasze dane zawierają zarówno dane numeryczne jak i kategoryczne. Niektóre kategorie reprezentowane są przez wartości liczbowe, a niektóre przez tekst.

Na podstawie metody info() wiemy równiez, ze nie wszystkie kolumny mają zmienne wypełnione całkowicie.

Większość algorytmów ML nie radzi sobie z brakami danych. Istnieją trzy podstawowe opcje jak mozemy sobie z tym poradzić: 1. usunięcie wierszy w których pojawiają się jakieś braki danych. 2. usunięcie całej kolumny gdzie występują braki danych 3. Wypełnienie brakujących wartości (imputacja danych) zerem, wartością średnią, lub medianą.

# opcja 1 - tylko 2 pasazerow nie maja Embarked - nie znamy portu docelowego - mozemy usunac te wiersze
train = train.dropna(subset=['Embarked'])

# opcja 2 - tutaj mamy tylko 204 wiersze z wartosciami w kolumnie Cabin - mozemy usunac te kolumne
train = train.drop("Cabin", axis=1)

# opcja 3 - znamy wiek 714 pasazerow. Dlatego opcja 2 nie jest dobra. Opcja 1 tez nie jest dobra bo usuniemy $22\%$ danych.
mean = train['Age'].mean()
train['Age'] = train['Age'].fillna(mean)

train.info()
<class 'pandas.core.frame.DataFrame'>
Index: 889 entries, 0 to 890
Data columns (total 11 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   PassengerId  889 non-null    int64  
 1   Survived     889 non-null    int64  
 2   Pclass       889 non-null    int64  
 3   Name         889 non-null    object 
 4   Sex          889 non-null    object 
 5   Age          889 non-null    float64
 6   SibSp        889 non-null    int64  
 7   Parch        889 non-null    int64  
 8   Ticket       889 non-null    object 
 9   Fare         889 non-null    float64
 10  Embarked     889 non-null    object 
dtypes: float64(2), int64(5), object(4)
memory usage: 83.3+ KB
print('Zmienna PassengerId ma {} roznych wartosci'.format(train['PassengerId'].nunique()))
print('Zmienna Name ma {} roznych wartosci'.format(train['Name'].nunique()))
print('Zmienna Ticket ma {} roznych wartosci'.format(train['Ticket'].nunique()))
Zmienna PassengerId ma 889 roznych wartosci
Zmienna Name ma 889 roznych wartosci
Zmienna Ticket ma 680 roznych wartosci
train = train.drop(['PassengerId', 'Name', 'Ticket'], axis=1)

train.info()
<class 'pandas.core.frame.DataFrame'>
Index: 889 entries, 0 to 890
Data columns (total 8 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   Survived  889 non-null    int64  
 1   Pclass    889 non-null    int64  
 2   Sex       889 non-null    object 
 3   Age       889 non-null    float64
 4   SibSp     889 non-null    int64  
 5   Parch     889 non-null    int64  
 6   Fare      889 non-null    float64
 7   Embarked  889 non-null    object 
dtypes: float64(2), int64(4), object(2)
memory usage: 62.5+ KB

Zmienne tekstowe

from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()

for col in ['Sex','Embarked']:
    le.fit(train[col])
    train[col] = le.transform(train[col])

train.head()
Survived Pclass Sex Age SibSp Parch Fare Embarked
0 0 3 1 22.0 1 0 7.2500 2
1 1 1 0 38.0 1 0 71.2833 0
2 1 3 0 26.0 0 0 7.9250 2
3 1 1 0 35.0 1 0 53.1000 2
4 0 3 1 35.0 0 0 8.0500 2

Skalowanie danych

print('max wieku to {}'.format(train['Age'].max())) 
print('max zmiennej Fare to {}'.format(train['Fare'].max()))
max wieku to 80.0
max zmiennej Fare to 512.3292
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
scaler.fit(train[['Age', 'Fare']])
train[['Age', 'Fare']] = scaler.transform(train[['Age', 'Fare']])
train.head()
Survived Pclass Sex Age SibSp Parch Fare Embarked
0 0 3 1 0.271174 1 0 0.014151 2
1 1 1 0 0.472229 1 0 0.139136 0
2 1 3 0 0.321438 0 0 0.015469 2
3 1 1 0 0.434531 1 0 0.103644 2
4 0 3 1 0.434531 0 0 0.015713 2
# test - uwaga na zwracany typ danych
sc = MinMaxScaler()
sc.fit(train)
tr=sc.transform(train)
print(type(tr),tr)
<class 'numpy.ndarray'> [[0.         1.         1.         ... 0.         0.01415106 1.        ]
 [1.         0.         0.         ... 0.         0.13913574 0.        ]
 [1.         1.         0.         ... 0.         0.01546857 1.        ]
 ...
 [0.         1.         0.         ... 0.33333333 0.04577135 1.        ]
 [1.         0.         1.         ... 0.         0.0585561  0.        ]
 [0.         1.         1.         ... 0.         0.01512699 0.5       ]]
#### Podział na zbiór treningowy i testowy
from sklearn.model_selection import train_test_split

input_data = train.iloc[:, 1:8]
labels = train.iloc[:,0]

tr_input, test_input, tr_labels, test_labels = train_test_split(input_data, labels, test_size=0.2, random_state=42)
tr_input.shape, test_input.shape
((711, 7), (178, 7))
import numpy as np 
with open('../data/train.npy', 'wb') as f:
    np.save(f, tr_input)
    np.save(f, tr_labels)

with open('../data/test.npy', 'wb') as f:
    np.save(f, test_input)
    np.save(f, test_labels)

Klasyfikatory

import random
random.seed(42)

# losowa funkcja klasyfikująca
def classify(passanger):
    return random.randint(0,1)

# pomocnicza funkcja
def run(f_classufy, x):
    return list(map(f_classufy, x))

def evaluate(predictions, actual):
    correct = list(filter(
        lambda item: item[0] == item[1],
        list(zip(predictions, actual))
    ))
    return f"{len(correct)} poprawnych przewidywan z {len(actual)}. Accuracy ({len(correct)/len(actual)*100:.0f}%)"
evaluate(run(classify, tr_input.values), tr_labels.values)
'348 poprawnych przewidywan z 711. Accuracy (49%)'
def kill_bill(item):
    return 0
evaluate(run(kill_bill, tr_input.values), tr_labels.values)
'440 poprawnych przewidywan z 711. Accuracy (62%)'
from sklearn.metrics import confusion_matrix

predictions = run(kill_bill, tr_input.values)
confusion_matrix(tr_labels.values, predictions)
# TN, FP, FN, TP
array([[440,   0],
       [271,   0]])
from sklearn.metrics import precision_score, recall_score, f1_score

print(precision_score(tr_labels.values, predictions))
print(recall_score(tr_labels.values, predictions))
print(f1_score(tr_labels.values, predictions))

# specificity = \sum TrueNegatives / \sum ALLActualNegatives
# npv = \sum TrueNegatives / \sum AllPredictedNegatives

def specificity(matrix):
    return matrix[0,0]/(matrix[0][0]+matrix[0][1]) if (matrix[0][0]+matrix[0][1] > 0) else 0

def npv(matrix):
    return matrix[0,0]/(matrix[0][0]+matrix[1][0]) if (matrix[0][0]+matrix[1][0] > 0) else 0

cm = confusion_matrix(tr_labels.values, predictions)
print("specificity",specificity(cm))
print("npv",npv(cm))
0.0
0.0
0.0
specificity 1.0
npv 0.6188466947960619
/Users/air/Desktop/quarto_projects/intro_to_qml/venv/lib/python3.10/site-packages/sklearn/metrics/_classification.py:1471: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 due to no predicted samples. Use `zero_division` parameter to control this behavior.
  _warn_prf(average, modifier, msg_start, len(result))

Zrob obliczenia dla losowego klasyfikatora!

def raport(name, run, classify, input, labels):
    cr_predictions = run(classify, input.values)
    cr_cm = confusion_matrix(labels.values, cr_predictions)
    cr_prcision  = precision_score(labels.values, cr_predictions)
    cr_recall = recall_score(labels.values, cr_predictions)
    cr_scpecificity = specificity(cr_cm)
    cr_npv = npv(cr_cm)
    cr_level = 0.25*(cr_prcision + cr_recall + cr_scpecificity + cr_npv)
    print(f"{name} precision {cr_prcision:.2f} recall {cr_recall:.2f} specificity {cr_scpecificity:.2f} npv {cr_npv:.2f} level {cr_level:.2f}")
    
raport("losowy", run, classify, tr_input, tr_labels)
raport("kill bill", run, kill_bill, tr_input, tr_labels)
losowy precision 0.38 recall 0.53 specificity 0.47 npv 0.62 level 0.50
kill bill precision 0.00 recall 0.00 specificity 1.00 npv 0.62 level 0.40
/Users/air/Desktop/quarto_projects/intro_to_qml/venv/lib/python3.10/site-packages/sklearn/metrics/_classification.py:1471: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 due to no predicted samples. Use `zero_division` parameter to control this behavior.
  _warn_prf(average, modifier, msg_start, len(result))
import qiskit
qiskit.__qiskit_version__
{'qiskit-terra': '0.25.1', 'qiskit': '0.44.1', 'qiskit-aer': '0.12.2', 'qiskit-ignis': '0.7.1', 'qiskit-ibmq-provider': '0.20.2', 'qiskit-nature': None, 'qiskit-finance': '0.3.4', 'qiskit-optimization': '0.5.0', 'qiskit-machine-learning': '0.6.1'}
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, execute, Aer
from math import sqrt

qc = QuantumCircuit(1)
initial_state = [1/sqrt(2), 1/sqrt(2)]
qc.initialize(initial_state, 0)
qc.measure_all()

Powyzszy obwód realizuje stan superpozycji i zwraca w losowy sposób wynik \(0\) lub \(1\). Oznacza to, ze moze byc kandydatem na klasyfikator binarny.

Zdefiniujmy powyzszy obwód tak aby realizowany był jako funkcja, którą mozemy wykorzystać w naszym problemie klasyfikacji.

from qiskit import execute, Aer, QuantumCircuit
from math import sqrt 
from sklearn.metrics import confusion_matrix, precision_score, recall_score 

def pqc_classify(backend, passenger_state):
    qc = QuantumCircuit(1)
    qc.initialize(passenger_state, 0)
    qc.measure_all()
    result = execute(qc, backend, shots=1).result()
    counts = result.get_counts()
    return int(list(map(lambda item: item[0], counts.items()))[0])

backend = Aer.get_backend('qasm_simulator')
initial_state = [1/sqrt(2), 1/sqrt(2)]
raport("Random PQC", run, lambda x: pqc_classify(backend, initial_state), 
       tr_input, tr_labels)
Random PQC precision 0.36 recall 0.47 specificity 0.47 npv 0.59 level 0.47

Powyzsze kody realizują klasyfikatory, które nie zalezą od naszych danych pasazerów.

  1. Preprocessing - przetworznie danych wejściowych do postaci przetwarzanej przez nasz obwód kwantowy. Wykorzystamy PQC. Ta część jest związana z klasycznym przetworzeniem danych i utworzeniem embeddingu.
  2. PQC
  3. Postprocessing - Nasz klasyfikator powinien zwracać wartość 0 lub 1. Tutaj powinien odbywać się proces przetłumaczenia wyniku realizowanego przez jakiś obwód kwanotwy na binarny wynik klasyfikacji. Tutaj równiez uzyjemy PQC do klasycznego przetworzenia.

Tylko druga część będzie w pełni realizowała obwód kwantowy. Łącząc wszystko razem otrzymujemy Wariacyjny hybrydowy klasyczno-kwantowy algorytm. Jest to jedno z najczęściej uzywanych podejść do modelowania danych klasycznych.

# 1 preprocessing
def pre_process(passanger):
    quantum_state = [1/sqrt(2), 1/sqrt(2)]
    return quantum_state

# 2. pqc

def pqc(beckend, quantum_state):
    qc = QuantumCircuit(1)
    qc.initialize(quantum_state, 0)
    qc.measure_all()
    result = execute(qc, backend, shots=1).result()
    counts = result.get_counts(qc)
    return counts

# 3. postprocessing
def post_process(counts):
    return int(list(map(lambda item: item[0], counts.items()))[0])

backend = Aer.get_backend('qasm_simulator')

raport("Variational Classifier", run, lambda passenger: post_process(pqc(backend, pre_process(passenger))), 
       tr_input, tr_labels)
Variational Classifier precision 0.41 recall 0.56 specificity 0.51 npv 0.65 level 0.54

Dane kazdego pasazera składają się z 7 zmiennych. Ze względu, iz nie chcemy (na razie) zmieniać ostatniego kroku musimy znaleźć jakąś metodę pozwalającą przypisać 7 zmiennym prawdopodobieństwo przezycia i śmierci. W ostatnim kroku odczytujemy po pomiarze tylko te dwie wielkości.

Znalezienie prawdopodobieństwa dla 7 zmiennych nie jest prostym zadaniem (w końcu to robią nasze klasyczne modele ML). Jendak mozemy zacząć od bardzo statystycznego podejścia. Zakładamy, ze zmienne są od siebie niezalezne i kazda zmienna z jakąś wagą przyczynia się do wartości prawdopodobieństwa przezycia. \[ P(survival) = \sum (F \mu_F) \]

def waga_zmiennej(feature, weight):
    return feature*weight

from functools import reduce

def get_overall_probablity(features, weights):
    return reduce(lambda result, data: result + waga_zmiennej(*data), 
                  zip(features, weights),
                  0
                  )

Jak zbudować wektor wag?

Zacznijmy od współczynnika korelacji.

from scipy.stats import spearmanr

columns = [list(map(lambda passneger: passneger[i], tr_input.values)) for i in range(0,7)]
correlations = list(map(lambda col: spearmanr(col, tr_labels.values)[0], columns))
correlations
[-0.33362848376406934,
 -0.5327583106581802,
 -0.03158046336028065,
 0.0688875885695018,
 0.12641683959850614,
 0.3105976636091728,
 -0.16652847475942076]

Zastosujmy to do pre-processingu

from math import pi, sin, cos 

def get_state(theta):
    return [cos(theta/2), sin(theta/2)]

def pre_process_weighted(passenger):
    mu = get_overall_probablity(passenger, correlations)
    # theta między 0 i pi  0 = |0> a pi = |1>
    quantum_state = get_state((1-mu)*pi)
    return quantum_state
backend = Aer.get_backend('statevector_simulator')

raport("Variational Classifier - train", run, lambda passenger: post_process(pqc(backend, pre_process_weighted(passenger))), 
       tr_input, tr_labels)
raport("Variational Classifier- test", run, lambda passenger: post_process(pqc(backend, pre_process_weighted(passenger))), 
       test_input, test_labels)
Variational Classifier - train precision 0.28 recall 0.35 specificity 0.45 npv 0.53 level 0.40
Variational Classifier- test precision 0.33 recall 0.39 specificity 0.50 npv 0.56 level 0.44