====== Laboratorium 8 - regresja logistyczna ======
Celem jest budowa modelu regresji logistycznej pozwalającej przewidywać, czy dany student zdał egzamin z języka C++ w pierwszym terminie.
Implementujemy oprogramowanie w języku Java.
=== Dla przypomnienia ===
* Regresja logistyczna jest metodą klasyfikacji binarnej.
* Posługuje się pojęciem //szansy// $o=\frac{p}{1-p}$
* Modeluje powiązanie prawdopodobieństwa etykiety 1 z modelem liniowym jako $logit(p) = ln(\frac{p}{1-p})=w^T x$
* Po przekształceniach: prawdopodobieństwo etykiety 1 opisane jest wzorem $p=\frac{1}{1+exp(-w^T x)}$
Patrz: [[https://home.agh.edu.pl/~pszwed/wiki/lib/exe/fetch.php?media=med:med-w04.pdf|wykład 4]]
=== Zbiory danych ===
* {{ :ed:egzamin-cpp.csv |egzamin-cpp.csv}} - poddane anonimizacji wyniki zaliczeń/egzaminu z C i C++ w 2016 roku
* {{ :ed:grid.csv |grid.csv}} - kombinacje ocen
**egzamin-cpp.csv**
ImieNazwisko;OcenaC;DataC;OcenaCpp;Egzamin
Dqhoil Dhxpluj;3.5;2016-01-14;4;3
Bhnhgpxj Lwjmq;4.5;2016-01-14;4;3
Wkgjnerme Djfbw;4;2016-01-20;3;2
Sredvmuwt Tcimknl;4.5;2016-01-20;4.5;3.5
Tiowe Bqoilnqbrx;4;2016-01-14;4.5;3
Bvaysqv Wuyih;3.5;2016-01-14;5;3
Jjoaxp Ktapcy;5;2016-01-20;4;3.5
**grid.csv**
ImieNazwisko,OcenaC,DataC,OcenaCpp
'Xxxxx Yyyyyy',3,2016-01-17,2
'Xxxxx Yyyyyy',3,2016-01-17,3
'Xxxxx Yyyyyy',3,2016-01-17,3.5
'Xxxxx Yyyyyy',3,2016-01-17,4
'Xxxxx Yyyyyy',3,2016-01-17,4.5
'Xxxxx Yyyyyy',3,2016-01-17,5
'Xxxxx Yyyyyy',3.5,2016-01-17,2
'Xxxxx Yyyyyy',3.5,2016-01-17,3
'Xxxxx Yyyyyy',3.5,2016-01-17,3.5
'Xxxxx Yyyyyy',3.5,2016-01-17,4
'Xxxxx Yyyyyy',3.5,2016-01-17,4.5
'Xxxxx Yyyyyy',3.5,2016-01-17,5
'Xxxxx Yyyyyy',4,2016-01-17,2
'Xxxxx Yyyyyy',4,2016-01-17,3
'Xxxxx Yyyyyy',4,2016-01-17,3.5
'Xxxxx Yyyyyy',4,2016-01-17,4
'Xxxxx Yyyyyy',4,2016-01-17,4.5
'Xxxxx Yyyyyy',4,2016-01-17,5
'Xxxxx Yyyyyy',4.5,2016-01-17,2
'Xxxxx Yyyyyy',4.5,2016-01-17,3
'Xxxxx Yyyyyy',4.5,2016-01-17,3.5
'Xxxxx Yyyyyy',4.5,2016-01-17,4
'Xxxxx Yyyyyy',4.5,2016-01-17,4.5
'Xxxxx Yyyyyy',4.5,2016-01-17,5
'Xxxxx Yyyyyy',5,2016-01-17,2
'Xxxxx Yyyyyy',5,2016-01-17,3
'Xxxxx Yyyyyy',5,2016-01-17,3.5
'Xxxxx Yyyyyy',5,2016-01-17,4
'Xxxxx Yyyyyy',5,2016-01-17,4.5
'Xxxxx Yyyyyy',5,2016-01-17,5
===== 1. Ładowanie danych i przetwarzanie wstępne =====
**1.** Utwórz sesję Sparka i załaduj zbiór danych. Wyświetl zawartość i schemat
SparkSession spark = SparkSession.builder()
.appName("LogisticRegressionOnExam")
.master("local")
.getOrCreate();
...
root
|-- ImieNazwisko: string (nullable = true)
|-- OcenaC: double (nullable = true)
|-- DataC: date (nullable = true)
|-- OcenaCpp: double (nullable = true)
|-- Egzamin: double (nullable = true)
**2.** Regresja logistyczna wymaga, aby atrybutów wejściowe były typu numerycznego. Jest też metodą klasyfikacji binarnej (etykiety powinny mieć wartości 0 i 1)
* przekonwertuj datę za pomocą funkcji ''unix_timestamp'' - nadaj nowej kolumnie nazwę ''timestamp''
* Dodaj kolumnę ''Wynik'' będącą wynikiem testu, czy ''Egzamin>=3.0'' - użyj funkcji SQL IF()
+-----------------+------+----------+--------+-------+----------+-----+
| ImieNazwisko|OcenaC| DataC|OcenaCpp|Egzamin| timestamp|Wynik|
+-----------------+------+----------+--------+-------+----------+-----+
| Dqhoil Dhxpluj| 3.5|2016-01-14| 4.0| 3.0|1452726000| 1|
| Bhnhgpxj Lwjmq| 4.5|2016-01-14| 4.0| 3.0|1452726000| 1|
| Wkgjnerme Djfbw| 4.0|2016-01-20| 3.0| 2.0|1453244400| 0|
|Sredvmuwt Tcimknl| 4.5|2016-01-20| 4.5| 3.5|1453244400| 1|
| Tiowe Bqoilnqbrx| 4.0|2016-01-14| 4.5| 3.0|1452726000| 1|
| Bvaysqv Wuyih| 3.5|2016-01-14| 5.0| 3.0|1452726000| 1|
| Jjoaxp Ktapcy| 5.0|2016-01-20| 4.0| 3.5|1453244400| 1|
| Mkengbtw Aainhh| 3.5|2016-01-20| 3.0| 2.0|1453244400| 0|
| Fbffjb Muupwshu| 4.0|2016-01-14| 5.0| 4.0|1452726000| 1|
| Yahwfyp Bvnlsig| 5.0|2016-01-14| 4.5| 4.0|1452726000| 1|
+-----------------+------+----------+--------+-------+----------+-----+
===== 2. LogisticRegressionAnalysis - analiza działania algorytmu =====
==== 2.1 Budowa modelu i interpretacja współczynników ====
**1.** Skonfiguruj algorytm i zbuduj model
LogisticRegression lr = new LogisticRegression()
.setMaxIter(100)
.setRegParam(0.1)
.setElasticNetParam(0)
.setFeaturesCol("features")
.setLabelCol("Wynik");
LogisticRegressionModel lrModel = lr.fit(df);
**2.** Napisz kod, który drukuje równanie regresji logistycznej
Oczekiwany wynik:
logit(zdal) = 0.719097*OcenaC + -0.000000*timestamp + 0.993461*OcenaCPP + 118.340611
**3.** Zinterpretuj współczynniki równania regresji (napisz kod lub zamieść wykonane obliczenia). Pamiętaj, że timestamp jest wyrażony w sekundach.
Poniższe wyniki były wygenerowane programowo. W praktyce wynik nie zależy od daty...
Wzrost OcenaC o 1 zwiększa logit o 0.719097, a szanse zdania razy 2.052578 czyli o 105.257821%
Wzrost DataC o 1 dzień zwiększa logit o -0.000000,a szanse zdania razy 0.992648 czyli o -0.735167%
Wzrost OcenaCPP o 1 zwiększa logit o 0.719097,a szanse zdania razy 2.700564 czyli o 170.056381%
==== 2.2 Predykcja i jej wyniki ====
**1.** Wywołaj funkcję predykcji i wyświetl dane...
Dataset df_with_predictions=lrModel.transform(df_trans);
var df_predictions = df_with_predictions
.select("features","rawPrediction","probability","prediction");
**2.** Napisz funkcję, która wyświetli informacje dotyczące predykcji
private static void analyzePredictions(Dataset dfPredictions,LogisticRegressionModel lrModel) {
dfPredictions.foreach(new ForeachFunction() {
...
});
}
Wewnątrz:
* oblicz wartośc ''logit'' jako iloczyn skalarny współczynników i cech powiększony o ''lrModel.intercept()''
* Oblicz prawdopodobieństwo P(0) i P(1) z odpowiedniego wzoru - wykorzystując wartośc logit
* Wyświetl i porównaj wartości ''rawPrediction'' i ''prawdopodobieństwa''
* Wyświetl prawdopodobieństwo wybranej przez klasyfikator etykiety - czyli większe z prawdopodobieństw
* Funkcja ''row.getAs(String)'' zwraca element w danej kolumnie. Użyj też funkcji ''Vector.argmax()''
==== 2.3 Dodaj do zbioru prawdopodobieństwo wybranej etykiety ====
Zadaniem jest dodanie do zbioru danych kolumny ''prob'' zawierającej wartość prawdopodobieństwa przypisanego etykiecie. Musimy wyodrębnić je z atrybutu ''probability''. W tym celu użyjemy mechanizmu User Defined Function (UDF). UDF to funkcja użytkownika rozszerzająca interfejs funkcjonalny Sparka. Funkcja musi zostać zarejestrowana w sesji.
**1.** Dodaj klasę zagnieżdżoną:
static class MaxVectorElement implements UDF1 {
@Override
public Double call(Vector vector) throws Exception {
return // największy element wektora - czyli o indeksie vector.argmax()
}
}
**2.** Zarejestruj funkcję (zazwyczaj bezpośrednio po utworzeniu sesji)
spark.udf().register( "max_vector_element",new MaxVectorElement(),DataTypes.DoubleType);
Alternatywnym rozwiązaniem może być rejestracja funkcji jako wyrażenia lambda, bez deklarowania klasy
UDF1 mve = v-> ???;
spark.udf().register( "max_vector_element_alt",mve,DataTypes.DoubleType);
**3.** Przekonwertuj zbiór z wynikami predykcji. Usuń kolumny typu ''features'' i ''rawPredictions'' Dodaj kolumnę ''prob'' z rezultatami wywołania UDF
.withColumn("prob",callUDF("max_vector_element",col("probability")))...
Oczekiwany wynik:
+-----------------+------+----------+--------+-------+----------+-----+----------+------------------+
| ImieNazwisko|OcenaC| DataC|OcenaCpp|Egzamin| timestamp|Wynik|prediction| prob|
+-----------------+------+----------+--------+-------+----------+-----+----------+------------------+
| Dqhoil Dhxpluj| 3.5|2016-01-14| 4.0| 3.0|1452726000| 1| 1.0|0.6822140478017863|
| Bhnhgpxj Lwjmq| 4.5|2016-01-14| 4.0| 3.0|1452726000| 1| 1.0| 0.815034643789244|
| Wkgjnerme Djfbw| 4.0|2016-01-20| 3.0| 2.0|1453244400| 0| 1.0|0.5214319094648453|
|Sredvmuwt Tcimknl| 4.5|2016-01-20| 4.5| 3.5|1453244400| 1| 1.0|0.8738590749460415|
| Tiowe Bqoilnqbrx| 4.0|2016-01-14| 4.5| 3.0|1452726000| 1| 1.0| 0.834828781680339|
+-----------------+------+----------+--------+-------+----------+-----+----------+------------------+
==== 2.4 Zapisz zbiór z wynikami ====
df_predictions = df_predictions.repartition(1);
df_predictions.write()
.format("csv")
.option("header", true)
.option("delimiter",",")
.mode(SaveMode.Overwrite)
.save("????/egzamin-with-classification.csv");
Instrukcja ''df_predictions = df_predictions.repartition(1);'' jest opcjonalna. Jaka będzie postać wyjścia, kiedy zmienimy argument ''repartition'' - np. ustawimy 5.
===== 3. LogisticRegressionScores - ocena wyników =====
Napisz funkcję trainAndTest, która:
* dokona podziału na zbiór treningowy i testowy
* skonfiguruje algorytm
* i przeprowadzi uczenie na zbiorze treningowym
static LogisticRegressionModel trainAndTest(Dataset df){
int splitSeed = 123;
Dataset[] splits = df.randomSplit(new double[]{0.7, 0.3},splitSeed);
Dataset df_train = splits[0];
Dataset df_test = splits[1];
LogisticRegression lr = new LogisticRegression()
.setMaxIter(20)
.setRegParam(0.1)
.setFeaturesCol("features")
.setLabelCol("Wynik");
LogisticRegressionModel lrModel = lr.fit(df_train);
...
return lrModel;
}
==== 3.1 Analiza informacji zebranych w training summary ====
BinaryLogisticRegressionTrainingSummary trainingSummary = lrModel.binarySummary();
**1.** Pobierz historię objective history i wyświetl jej wykres. Napisz odpowiednią funkcję do wyświetlania wykresu ''plotObjectiveHistory()''
double[] objectiveHistory = trainingSummary.objectiveHistory();
plotObjectiveHistory(objectiveHistory);
Oczekiwany wynik:
{{ :ed:logreg-objective-history.png?direct&400 |}}
**2.** Pobierz informacje o krzywej ROC. Możesz o niej przeczytać tu: [[https://home.agh.edu.pl/~pszwed/wiki/lib/exe/fetch.php?media=med:med-w04.pdf|Wykład 4, slajdy 24-27]]
Dataset roc = trainingSummary.roc();
roc.show();
+--------------------+-------------------+
| FPR| TPR|
+--------------------+-------------------+
| 0.0| 0.0|
| 0.0|0.07692307692307693|
| 0.0|0.09615384615384616|
| 0.0|0.15384615384615385|
| 0.0|0.21153846153846154|
| 0.0|0.23076923076923078|
| 0.0| 0.3076923076923077|
| 0.0|0.34615384615384615|
| 0.0|0.38461538461538464|
| 0.0| 0.4230769230769231|
| 0.0| 0.4807692307692308|
| 0.0| 0.5|
| 0.0| 0.5192307692307693|
| 0.0| 0.5576923076923077|
| 0.0| 0.5961538461538461|
|0.047619047619047616| 0.5961538461538461|
|0.047619047619047616| 0.6153846153846154|
|0.047619047619047616| 0.6730769230769231|
| 0.09523809523809523| 0.6730769230769231|
| 0.14285714285714285| 0.6923076923076923|
+--------------------+-------------------+
**3.** Wyświetl wykres ROC. Napisz odpowiednią funkcję
static void plotROC(Dataset roc)
Oczekiwany wynik:
{{ :ed:logrec-roc-curve.png?direct&400 |}}
**3.** Wyświetl miary:
* Accuracy
* FPR
* TPR
* Precision
* Recall
* F-measure
==== 3.2 Dobór progu prawdopodobieństwa ====
Krzywą ROC można wykorzystać do doboru progu prawdopodobieństwa. Patrz [[https://home.agh.edu.pl/~pszwed/wiki/lib/exe/fetch.php?media=med:med-w04.pdf|Wykład 4 slajd 27]]
**1.** Dobierzemy próg według miary //F-measure//. To będzie gdzieś tu:
Dataset df_fmeasures = trainingSummary.fMeasureByThreshold();
df_fmeasures.offset(35).show();
+-------------------+------------------+
| threshold| F-Measure|
+-------------------+------------------+
| 0.4627032508959811|0.8869565217391304|
| 0.4382847817892507|0.8793103448275861|
| 0.4034706528697256| 0.888888888888889|
| 0.3858997834933381|0.8813559322033898|
| 0.3461380134069699| 0.859504132231405|
| 0.3143853597224281|0.8524590163934427|
|0.19135299955580787|0.8455284552845529|
| 0.1472470120383692|0.8387096774193548|
|0.13010893832947723| 0.832|
+-------------------+------------------+
**2.** Wyznacz programowo najlepszy próg.
* Wpierw wyznacz maksymalną wartość F-measure (przykład kodu poniżej)
* A następnie odpowiadającą jej wartość progu (używając ''where ... equalTo ... select...'')
* W powyższej tabelce możesz sprawdzić, czy znalezione zostały właściwe wartości (oczywiście one zależą od sposobu podziału na zbiór treningowy i testowy, a więc ziarna ''seed'')
double maxFMeasure = df_fmeasures.select(functions.max("F-Measure")).head().getDouble(0);
**3.** Ustaw próg klasyfikatora
lrModel.setThreshold(bestThreshold);
==== 3.3 Ewaluacja na zbiorze testowym ====
**1.** Wywołaj funkcję predykcji i skonfiguruj ewaluator
Dataset predictions = lrModel.transform(df_test);
MulticlassClassificationEvaluator eval = new MulticlassClassificationEvaluator()
.setLabelCol("Wynik")
.setPredictionCol("prediction");
**2.** Wyznacz:
* accuracy
* weightedPrecision
* weightedRecall
* f1
Oczekiwane są wartości rzędu 0.82-0.83
Nazwy dostępnych metryk:
(f1|accuracy|weightedPrecision|weightedRecall|weightedTruePositiveRate| weightedFalsePositiveRate|weightedFMeasure|truePositiveRateByLabel| falsePositiveRateByLabel|precisionByLabel|recallByLabel|fMeasureByLabel| logLoss|hammingLoss)'
===== 4. LogisticRegressionGrid - tworzenie tabeli ocen =====
Celem jest utworzenie tabeli ocen postaci, jak poniżej
+--------------+------+----------+--------+--------+
| ImieNazwisko|OcenaC| DataC|OcenaCpp| Wynik|
+--------------+------+----------+--------+--------+
|'Xxxxx Yyyyyy'| 3.0|2016-01-17| 2.0|Nie zdał|
|'Xxxxx Yyyyyy'| 3.0|2016-01-17| 3.0|Nie zdał|
|'Xxxxx Yyyyyy'| 3.0|2016-01-17| 3.5| Zdał|
|'Xxxxx Yyyyyy'| 3.0|2016-01-17| 4.0| Zdał|
|'Xxxxx Yyyyyy'| 3.0|2016-01-17| 4.5| Zdał|
|'Xxxxx Yyyyyy'| 3.0|2016-01-17| 5.0| Zdał|
|'Xxxxx Yyyyyy'| 3.5|2016-01-17| 2.0|Nie zdał|
|'Xxxxx Yyyyyy'| 3.5|2016-01-17| 3.0|Nie zdał|
|'Xxxxx Yyyyyy'| 3.5|2016-01-17| 3.5| Zdał|
|'Xxxxx Yyyyyy'| 3.5|2016-01-17| 4.0| Zdał|
...
**Uwaga:** wynik może się nieco różnić w zależności od konfiguracji, np. progu prawdopodobieństwa
**1.** Wytrenuj klasyfikator na zbiorze ''egzamin-cpp.csv''. Wykorzystuj kod z poprzedniej części, ustaw próg prawdopodobieństwa.
**2.** Napisz funkcje
void addClassificationToGrid(SparkSession spark, LogisticRegressionModel lrModel)
która:
* Wczyta zbiór danych ''grid.csv''
* Przetworzy daty, tak aby stały się wartościami numerycznymi
* Skonfiguruje VectorAssembler
* Wywoła funkcję predykcji zmiennej ''lrMpdel''
* Usunie nadmiarowe kolumny
* Za pomocą funkcji ''IF()'' SQL lub zarejestrowanej funkcji użytkownika UDF dokona konwersji etykiet //0->Nie zdał// oraz //1->Zdał//
* Wyświetli wynik
* Zapisze w pliku ''grid-with-classification.csv''