Titanic¶
This example uses the titanic dataset to predict the survival of passengers on the Titanic. You can download the Jupyter Notebook of the study here.
We will follow the data science cycle (Data Exploration - Data Preparation - Data Modeling - Model Evaluation - Model Deployment) to solve this problem.
Initialization¶
This example uses the following version of VerticaPy:
import verticapy as vp
vp.__version__
Out[2]: '1.1.0'
Connect to Vertica. This example uses an existing connection called VerticaDSN .
For details on how to create a connection, see the Connection tutorial.
You can skip the below cell if you already have an established connection.
vp.connect("VerticaDSN")
Let’s create a Virtual DataFrame of the dataset.
from verticapy.datasets import load_titanic
titanic = load_titanic()
titanic.head(5)
123 pclass100% | ... | 123 body9% | Abc home.dest57% | |
| 1 | 1 | ... | 22 | Montevideo, Uruguay |
| 2 | 1 | ... | [null] | Trenton, NJ |
| 3 | 1 | ... | [null] | [null] |
| 4 | 1 | ... | [null] | Montevideo, Uruguay |
| 5 | 1 | ... | [null] | Los Angeles, CA |
Data Exploration and Preparation¶
Let’s explore the data by displaying descriptive statistics of all the columns.
titanic.describe(method = "categorical", unique = True)
| ... | top_percent | unique | |
| "pclass" | ... | 53.728 | 3.0 |
| "survived" | ... | 63.533 | 2.0 |
| "name" | ... | 0.162 | 1232.0 |
| "sex" | ... | 65.964 | 2.0 |
| "age" | ... | 19.206 | 96.0 |
| "sibsp" | ... | 67.747 | 7.0 |
| "parch" | ... | 76.904 | 8.0 |
| "ticket" | ... | 0.81 | 887.0 |
| "fare" | ... | 4.7 | 277.0 |
| "cabin" | ... | 76.823 | 182.0 |
| "embarked" | ... | 70.746 | 3.0 |
| "boat" | ... | 64.425 | 26.0 |
| "body" | ... | 90.438 | 118.0 |
| "home.dest" | ... | 42.788 | 359.0 |
The columns body (passenger ID), home.dest (passenger origin/destination), embarked (origin port) and ticket (ticket ID) shouldn’t influence survival, so we can ignore these.
Let’s focus our analysis on the columns name and cabin. We’ll begin with the passengers’ names.
from verticapy.machine_learning.vertica import CountVectorizer
model = CountVectorizer()
model.fit(titanic, ["Name"])
model.transform()
Abc token100% | ... | 123 df100% | 123 rnk100% | |
| 1 | mr | ... | 0.14816310052482842 | 1 |
| 2 | miss | ... | 0.046023415421881306 | 2 |
| 3 | mrs | ... | 0.03734356075898264 | 3 |
| 4 | william | ... | 0.016148566814695196 | 4 |
| 5 | john | ... | 0.013726281792490917 | 5 |
| 6 | master | ... | 0.011303996770286637 | 6 |
| 7 | henry | ... | 0.008881711748082359 | 7 |
| 8 | james | ... | 0.007468712151796528 | 8 |
| 9 | charles | ... | 0.007468712151796528 | 8 |
| 10 | thomas | ... | 0.006863140896245458 | 10 |
| 11 | joseph | ... | 0.006257569640694388 | 11 |
| 12 | george | ... | 0.006257569640694388 | 11 |
| 13 | edward | ... | 0.005450141299959629 | 13 |
| 14 | mary | ... | 0.005248284214775939 | 14 |
| 15 | elizabeth | ... | 0.004440855874041179 | 15 |
| 16 | johan | ... | 0.004440855874041179 | 15 |
| 17 | frederick | ... | 0.004037141703673799 | 17 |
| 18 | samuel | ... | 0.003835284618490109 | 18 |
| 19 | robert | ... | 0.003633427533306419 | 19 |
| 20 | alexander | ... | 0.003633427533306419 | 19 |
Passengers’ titles might come in handy. We can extract these from their names.
Let’s move on to the cabins.
model = CountVectorizer()
model.fit("titanic", ["cabin"])
model.transform()
Abc token99% | ... | 123 df100% | 123 rnk100% | |
| 1 | [null] | ... | 0.7337461300309598 | 1 |
| 2 | f | ... | 0.006191950464396285 | 2 |
| 3 | c27 | ... | 0.004643962848297214 | 3 |
| 4 | c25 | ... | 0.004643962848297214 | 3 |
| 5 | c23 | ... | 0.004643962848297214 | 3 |
| 6 | g6 | ... | 0.003869969040247678 | 6 |
| 7 | c78 | ... | 0.003095975232198142 | 7 |
| 8 | b96 | ... | 0.003095975232198142 | 7 |
| 9 | b98 | ... | 0.003095975232198142 | 7 |
| 10 | f4 | ... | 0.003095975232198142 | 7 |
| 11 | b57 | ... | 0.003095975232198142 | 7 |
| 12 | d | ... | 0.003095975232198142 | 7 |
| 13 | c26 | ... | 0.003095975232198142 | 7 |
| 14 | c22 | ... | 0.003095975232198142 | 7 |
| 15 | b66 | ... | 0.003095975232198142 | 7 |
| 16 | b59 | ... | 0.003095975232198142 | 7 |
| 17 | b63 | ... | 0.003095975232198142 | 7 |
| 18 | b60 | ... | 0.002321981424148607 | 18 |
| 19 | b58 | ... | 0.002321981424148607 | 18 |
| 20 | f33 | ... | 0.002321981424148607 | 18 |
Here, we have the cabin IDs, the letter of which represents a certain position on the boat. Let’s see how often each cabin occurs in the dataset.
model = CountVectorizer()
model.fit("titanic", ["cabin"])
model.transform()["token"].str_slice(1, 1).groupby(
columns = ["token"], expr = ["SUM(cnt)"]
).head(30)
Abc token88% | 123 SUM100% | |
| 1 | d | 47 |
| 2 | c | 113 |
| 3 | a | 20 |
| 4 | e | 43 |
| 5 | b | 92 |
| 6 | f | 19 |
| 7 | g | 9 |
| 8 | [null] | 948 |
| 9 | t | 1 |
While NULL values for “boat” clearly represent passengers who have a dedicated “lifeboat”, we can’t be so sure about NULL values for “cabin”. We can guess that these might represent passengers without a cabin. If this is the case, then these are missing values not at random (MNAR).
We’ll revisit this problem later. For now, let’s drop the columns that don’t affect survival and then encode the rest.
titanic.drop(["body", "home.dest", "embarked", "ticket"])
titanic["cabin"].str_slice(1, 1)["name"].str_extract(
' ([A-Za-z]+)\.')["boat"].fillna(
method = "0ifnull"
)["cabin"].fillna("No Cabin")
123 pclass100% | ... | 123 survived100% | 123 boat100% | |
| 1 | 1 | ... | 0 | 0 |
| 2 | 1 | ... | 0 | 0 |
| 3 | 1 | ... | 0 | 0 |
| 4 | 1 | ... | 0 | 0 |
| 5 | 1 | ... | 0 | 0 |
| 6 | 1 | ... | 0 | 0 |
| 7 | 1 | ... | 0 | 0 |
| 8 | 1 | ... | 0 | 0 |
| 9 | 1 | ... | 0 | 0 |
| 10 | 1 | ... | 0 | 0 |
| 11 | 1 | ... | 0 | 0 |
| 12 | 1 | ... | 0 | 0 |
| 13 | 1 | ... | 0 | 0 |
| 14 | 1 | ... | 0 | 0 |
| 15 | 1 | ... | 0 | 0 |
| 16 | 1 | ... | 0 | 0 |
| 17 | 1 | ... | 0 | 1 |
| 18 | 1 | ... | 0 | 0 |
| 19 | 1 | ... | 0 | 0 |
| 20 | 1 | ... | 0 | 0 |
Looking at our data now, we can see that some first class passengers have a NULL value for their cabin, so we can safely say that our assumption about the meaning of a NULL value of “cabin” turned out to be incorrect. This means that the “cabin” column has far too many missing values at random (MAR). We’ll have to drop it.
titanic["cabin"].drop()
123 pclass100% | ... | 123 fare99% | 123 boat100% | |
| 1 | 1 | ... | 49.5042 | 0 |
| 2 | 1 | ... | 35.5 | 0 |
| 3 | 1 | ... | 31.0 | 0 |
| 4 | 1 | ... | 47.1 | 0 |
| 5 | 1 | ... | 136.7792 | 0 |
| 6 | 1 | ... | 83.1583 | 0 |
| 7 | 1 | ... | 52.0 | 0 |
| 8 | 1 | ... | 106.425 | 0 |
| 9 | 1 | ... | 31.6792 | 0 |
| 10 | 1 | ... | 53.1 | 0 |
| 11 | 1 | ... | 79.2 | 0 |
| 12 | 1 | ... | 83.475 | 0 |
| 13 | 1 | ... | 0.0 | 0 |
| 14 | 1 | ... | 42.5 | 0 |
| 15 | 1 | ... | 51.8625 | 0 |
| 16 | 1 | ... | 52.0 | 0 |
| 17 | 1 | ... | 30.6958 | 1 |
| 18 | 1 | ... | 30.0 | 0 |
| 19 | 1 | ... | 75.2417 | 0 |
| 20 | 1 | ... | 51.8625 | 0 |
Let’s look at descriptive statistics of the entire Virtual Dataframe.
titanic.describe(method = "all")
| ... | Abc "name"100% | Abc "sex"100% | |
| dtype | ... | varchar(164) | varchar(20) |
| percent | ... | 100.0 | 100 |
| count | ... | 1234 | 1234 |
| top | ... | Mr. | male |
| top_percent | ... | 59.481 | 65.964 |
| avg | ... | 4.73176661264181 | 4.6807131280389 |
| stddev | ... | 1.08376455904966 | 0.948042321667023 |
| min | ... | 4 | 4 |
| approx_25% | ... | 4 | 4 |
| approx_50% | ... | 4 | 4 |
| approx_75% | ... | 5 | 6 |
| max | ... | 10 | 6 |
| range | ... | 6 | 2 |
| empty | ... | 0 | 0 |
Descriptive statistics can give us valuable insights into our data. Notice, for example, that the column “fare” has many outliers (The maximum of 512.33 is much greater than the 9th decile of 79.13). Most passengers traveled in 3rd class (median of pclass = 3).
The “sibsp” column represents the number of siblings for each passenger, while the “parch” column represents the number of parents and children. We can use these to create a new feature: “family_size”.
titanic["family_size"] = titanic["parch"] + titanic["sibsp"] + 1
Let’s move on to outliers. We have several tools for locating outliers (LocalOutlierFactor, DBSCAN, KMeans…), but we’ll just use winsorization in this example. Again, “fare” has many outliers, so we’ll start there.
titanic["fare"].fill_outliers(
method = "winsorize",
alpha = 0.03,
)
123 pclass100% | ... | 123 survived100% | 123 family_size100% | |
| 1 | 1 | ... | 0 | 1 |
| 2 | 1 | ... | 0 | 1 |
| 3 | 1 | ... | 0 | 1 |
| 4 | 1 | ... | 0 | 1 |
| 5 | 1 | ... | 0 | 2 |
| 6 | 1 | ... | 0 | 3 |
| 7 | 1 | ... | 0 | 2 |
| 8 | 1 | ... | 0 | 2 |
| 9 | 1 | ... | 0 | 1 |
| 10 | 1 | ... | 0 | 2 |
| 11 | 1 | ... | 0 | 1 |
| 12 | 1 | ... | 0 | 2 |
| 13 | 1 | ... | 0 | 1 |
| 14 | 1 | ... | 0 | 1 |
| 15 | 1 | ... | 0 | 1 |
| 16 | 1 | ... | 0 | 2 |
| 17 | 1 | ... | 0 | 1 |
| 18 | 1 | ... | 0 | 1 |
| 19 | 1 | ... | 0 | 1 |
| 20 | 1 | ... | 0 | 1 |
Let’s encode the column sex so we can use it with numerical methods.
titanic["sex"].label_encode()
123 pclass100% | ... | 123 survived100% | 123 family_size100% | |
| 1 | 1 | ... | 0 | 1 |
| 2 | 1 | ... | 0 | 1 |
| 3 | 1 | ... | 0 | 1 |
| 4 | 1 | ... | 0 | 1 |
| 5 | 1 | ... | 0 | 2 |
| 6 | 1 | ... | 0 | 3 |
| 7 | 1 | ... | 0 | 2 |
| 8 | 1 | ... | 0 | 2 |
| 9 | 1 | ... | 0 | 1 |
| 10 | 1 | ... | 0 | 2 |
| 11 | 1 | ... | 0 | 1 |
| 12 | 1 | ... | 0 | 2 |
| 13 | 1 | ... | 0 | 1 |
| 14 | 1 | ... | 0 | 1 |
| 15 | 1 | ... | 0 | 1 |
| 16 | 1 | ... | 0 | 2 |
| 17 | 1 | ... | 0 | 1 |
| 18 | 1 | ... | 0 | 1 |
| 19 | 1 | ... | 0 | 1 |
| 20 | 1 | ... | 0 | 1 |
The column age has too many missing values and since most machine learning algorithms can’t handle missing values, we need to impute our data. Let’s fill the missing values using the average age of the passengers that have the same “pclass” and “sex”.
titanic["age"].fillna(method = "mean", by = ["pclass", "sex"])
123 pclass100% | ... | 123 survived100% | 123 family_size100% | |
| 1 | 1 | ... | 0 | 1 |
| 2 | 1 | ... | 0 | 1 |
| 3 | 1 | ... | 0 | 1 |
| 4 | 1 | ... | 0 | 1 |
| 5 | 1 | ... | 0 | 2 |
| 6 | 1 | ... | 0 | 3 |
| 7 | 1 | ... | 0 | 2 |
| 8 | 1 | ... | 0 | 2 |
| 9 | 1 | ... | 0 | 1 |
| 10 | 1 | ... | 0 | 2 |
| 11 | 1 | ... | 0 | 1 |
| 12 | 1 | ... | 0 | 2 |
| 13 | 1 | ... | 0 | 1 |
| 14 | 1 | ... | 0 | 1 |
| 15 | 1 | ... | 0 | 1 |
| 16 | 1 | ... | 0 | 2 |
| 17 | 1 | ... | 0 | 1 |
| 18 | 1 | ... | 0 | 1 |
| 19 | 1 | ... | 0 | 1 |
| 20 | 1 | ... | 0 | 1 |
Let’s draw the correlation matrix to see the links between variables.
titanic.corr(method = "spearman")
Fare correlates strongly with family size. This is about what you would expect: a larger family means more tickets, and more tickets means a greater fare.
Survival correlates strongly with whether or not a passenger has a lifeboat (the boat variable). Still, to increase the generality of our model, we should avoid predictions based on just one variable. Let’s split the study into two use cases:
Passengers with a lifeboat
Passengers without a lifeboat
Before we move on: we did a lot of work to clean up this data, but we haven’t saved anything to our Vertica database! Let’s look at the modifications we’ve made to the vDataFrame.
print(titanic.current_relation())
(
SELECT
"pclass",
"survived",
"name",
"sex",
COALESCE("age", AVG("age") OVER (PARTITION BY "pclass", "sex")) AS "age",
"sibsp",
"parch",
"fare",
"boat",
"family_size"
FROM
(
SELECT
"pclass",
"survived",
REGEXP_SUBSTR("name", ' ([A-Za-z]+)\.') AS "name",
DECODE("sex", 'female', 0, 'male', 1, 2) AS "sex",
"age",
"sibsp",
"parch",
(CASE WHEN "fare" < -176.6204982585513 THEN -176.6204982585513 WHEN "fare" > 244.5480856064831 THEN 244.5480856064831 ELSE "fare" END) AS "fare",
DECODE("boat", NULL, 0, 1) AS "boat",
(("parch") + ("sibsp")) + (1) AS "family_size"
FROM
(
SELECT
"pclass",
"survived",
"name",
"sex",
"age",
"sibsp",
"parch",
"fare",
"boat"
FROM
"public"."titanic")
VERTICAPY_SUBTABLE)
VERTICAPY_SUBTABLE)
VERTICAPY_SUBTABLE
Let see what’s happening when we aggregate and turn on SQL generation.
vp.set_option("sql_on", True)
titanic_avg = titanic.avg()
<IPython.core.display.HTML object>
<IPython.core.display.HTML object>
VerticaPy dynamically generates SQL code whenever you make modifications to your data. To avoid recomputation, it also stores previous aggregations. If we filter anything in our data, it will update the catalog with our modifications.
vp.set_option("sql_on", False)
print(titanic.info())
The vDataFrame was modified many times:
* {Thu Oct 31 18:48:17 2024} [Drop]: vDataColumn "body" was deleted from the vDataFrame.
* {Thu Oct 31 18:48:17 2024} [Drop]: vDataColumn "home.dest" was deleted from the vDataFrame.
* {Thu Oct 31 18:48:17 2024} [Drop]: vDataColumn "embarked" was deleted from the vDataFrame.
* {Thu Oct 31 18:48:18 2024} [Drop]: vDataColumn "ticket" was deleted from the vDataFrame.
* {Thu Oct 31 18:48:19 2024} [Apply]: The vDataColumn 'cabin' was transformed with the func 'x -> SUBSTR("cabin", 1, 1)'.
* {Thu Oct 31 18:48:20 2024} [Apply]: The vDataColumn 'name' was transformed with the func 'x -> REGEXP_SUBSTR("name", ' ([A-Za-z]+)\.')'.
* {Thu Oct 31 18:48:22 2024} [Fillna]: 795 "boat" missing values were filled.
* {Thu Oct 31 18:48:25 2024} [Fillna]: 948 "cabin" missing values were filled.
* {Thu Oct 31 18:48:26 2024} [Drop]: vDataColumn "cabin" was deleted from the vDataFrame.
* {Thu Oct 31 18:48:30 2024} [Eval]: A new vDataColumn "family_size" was added to the vDataFrame.
* {Thu Oct 31 18:48:31 2024} [Apply]: The vDataColumn 'fare' was transformed with the func 'x -> (CASE WHEN "fare" < -176.6204982585513 THEN -176.6204982585513 WHEN "fare" > 244.5480856064831 THEN 244.5480856064831 ELSE "fare" END)'.
* {Thu Oct 31 18:48:33 2024} [Label Encoding]: Label Encoding was applied to the vDataColumn "sex" using the following mapping:
female => 0 male => 1
* {Thu Oct 31 18:48:36 2024} [Fillna]: 237 "age" missing values were filled.
Let’s move on to modeling our data. Save the vDataFrame to your Vertica database.
from verticapy.sql import drop
# Titanic Boat
drop("titanic_boat", method = "view")
Out[10]: True
titanic_boat = titanic.search(titanic["boat"] == 1).to_db("titanic_boat", relation_type = "view")
# Titanic No Boat
drop("titanic_no_boat", method = "view")
Out[12]: True
titanic_no_boat = titanic.search(titanic["boat"] == 0).to_db("titanic_no_boat", relation_type = "view")
Machine Learning¶
Passengers with a lifeboat¶
First, let’s look at the number of survivors.
titanic_boat["survived"].describe()
| value | |
| name | "survived" |
| dtype | integer |
| unique | 2.0 |
| count | 439.0 |
| 1 | 430 |
| 0 | 9 |
We have nine deaths. Let’s try to understand why these passengers died.
titanic_boat.search(titanic_boat["survived"] == 0).head(10)
123 pclass100% | ... | 123 boat100% | 123 family_size100% | |
| 1 | 1 | ... | 1 | 1 |
| 2 | 1 | ... | 1 | 1 |
| 3 | 2 | ... | 1 | 2 |
| 4 | 3 | ... | 1 | 2 |
| 5 | 3 | ... | 1 | 1 |
| 6 | 3 | ... | 1 | 2 |
| 7 | 3 | ... | 1 | 2 |
| 8 | 3 | ... | 1 | 1 |
| 9 | 3 | ... | 1 | 2 |
Apart from a decent amount of these passengers being third-class passengers, it doesn’t seem like there are any clear predictors here for their deaths. Making a model from this would be unhelpful.
Passengers without a lifeboat¶
Let’s move on to passengers without a lifeboat.
titanic_no_boat["survived"].describe()
| value | |
| name | "survived" |
| dtype | integer |
| unique | 2.0 |
| count | 795.0 |
| 0 | 775 |
| 1 | 20 |
Only 20 survived. Let’s find out why.
titanic_no_boat.search(titanic_boat["survived"] == 1).head(20)
123 pclass100% | ... | 123 boat100% | 123 family_size100% | |
| 1 | 1 | ... | 0 | 1 |
| 2 | 2 | ... | 0 | 1 |
| 3 | 2 | ... | 0 | 1 |
| 4 | 2 | ... | 0 | 2 |
| 5 | 2 | ... | 0 | 2 |
| 6 | 2 | ... | 0 | 1 |
| 7 | 2 | ... | 0 | 2 |
| 8 | 2 | ... | 0 | 4 |
| 9 | 2 | ... | 0 | 2 |
| 10 | 3 | ... | 0 | 2 |
| 11 | 3 | ... | 0 | 2 |
| 12 | 3 | ... | 0 | 1 |
| 13 | 3 | ... | 0 | 1 |
| 14 | 3 | ... | 0 | 1 |
| 15 | 3 | ... | 0 | 1 |
| 16 | 3 | ... | 0 | 4 |
| 17 | 3 | ... | 0 | 1 |
| 18 | 3 | ... | 0 | 1 |
| 19 | 3 | ... | 0 | 1 |
| 20 | 3 | ... | 0 | 1 |
Most survivors seem to be women. Let’s build a model with this in mind.
One of our predictors is categorical: the passenger title. Some of these predictors are corrleated, so it’d be best to work with a non-linear classifier that can handle that. In this case, a random forest classifier seems to be perfect. Let’s evaluate it with a cross-validation.
from verticapy.machine_learning.vertica import RandomForestClassifier
from verticapy.machine_learning.model_selection import cross_validate
predictors = titanic.get_columns(exclude_columns = ["survived"])
response = "survived"
model = RandomForestClassifier(
n_estimators = 40,
max_depth = 4,
)
cross_validate(model, titanic_no_boat, predictors, response)
| ... | csi | time | |
| 1-fold | ... | 0.2 | 17.374155044555664 |
| 2-fold | ... | 0.21428571428571427 | 17.296217679977417 |
| 3-fold | ... | 0.16666666666666666 | 17.17010188102722 |
| avg | ... | 0.19365079365079363 | 17.280158201853435 |
| std | ... | 0.019952071571391325 | 0.08407478224653067 |
This dataset is pretty unbalanced so we’ll use an AUC to evaluate it. Looking at our table, our model has an average AUC of more than 0.9, so our model is quite good.
We can now build a model with the entire dataset.
model.fit(titanic_no_boat, predictors, response)
===========
call_string
===========
SELECT rf_classifier('"public"."_verticapy_tmp_randomforestclassifier_v_mldb_c4bd13b097b811efa8720242ac120002_"', '"public"."_verticapy_tmp_view_v_mldb_efbce32497b811efa8720242ac120002_"', 'survived', '"pclass", "name", "sex", "age", "sibsp", "parch", "fare", "boat", "family_size"' USING PARAMETERS exclude_columns='', ntree=40, mtry=4, sampling_size=0.632, max_depth=4, max_breadth=1000000000, min_leaf_size=1, min_info_gain=0, nbins=32);
=======
details
=======
predictor | type
-----------+----------------
pclass | int
name |char or varchar
sex | int
age |float or numeric
sibsp | int
parch | int
fare |float or numeric
boat | int
family_size| int
===============
Additional Info
===============
Name |Value
------------------+-----
tree_count | 40
rejected_row_count| 1
accepted_row_count| 794
Let’s look at the importance of each feature.
model.features_importance()
As expected, the passenger’s title is the most important predictors of survival.
Conclusion¶
We’ve solved our problem in a Pandas-like way, all without ever loading data into memory!