Series 1.1.3 - Building logistic regression model using scikit-learn
Machine learning projects
Scikit-learn
Polars
Python
Jupyter
ChEMBL database
Cheminformatics
Author
Jennifer HY Lin
Published
January 4, 2023
Modified
November 1, 2024
Import libraries
This is the third post that follows on from the previous two about parquet file and data preprocessing, and it will need the following libraries to build and train a logistic regression (LR) model before using it to predict max phase outcome on a testing dataset by using scikit-learn.
## using magic pip to install sklearn & altair (somehow venv keeps switching off in vscode...)# %pip install -U scikit-learn# %pip install altairimport sklearnprint(f"scikit-learn version used is: {sklearn.__version__}")from sklearn import preprocessing, set_configfrom sklearn.model_selection import train_test_splitfrom sklearn.linear_model import LogisticRegressionfrom sklearn.pipeline import Pipelinefrom sklearn.preprocessing import StandardScalerimport polars as plprint(f"polars version used is: {pl.__version__}")import altair as altprint(f"altair version used is: {alt.__version__}")import pickleimport numpy as np
scikit-learn version used is: 1.5.0
polars version used is: 1.9.0
altair version used is: 5.4.1
The same set of data saved in the previous post will be read here using polars dataframe library.
df = pl.read_csv("df_ml.csv")df
shape: (5_670, 9)
Max_Phase
Polar Surface Area
HBA
HBD
#RO5 Violations
QED Weighted
CX LogP
CX LogD
Heavy Atoms
i64
f64
i64
i64
i64
f64
f64
f64
i64
0
66.81
4
1
0
0.47
3.94
3.94
32
0
62.55
3
1
0
0.93
3.38
3.38
25
0
73.86
5
1
2
0.12
9.34
9.34
40
0
84.22
4
2
0
0.76
2.01
-0.19
26
0
40.46
4
0
0
0.62
4.0
4.0
26
…
…
…
…
…
…
…
…
…
1
128.03
8
2
0
0.49
2.09
1.86
34
1
0.0
0
0
0
0.0
0.0
0.0
0
1
74.02
6
1
0
0.68
3.65
2.3
30
1
94.83
4
3
0
0.44
1.2
-1.18
12
1
95.92
6
1
0
0.9
1.66
1.66
18
Logistic regression with scikit-learn
LR is one of the supervised methods in the statistical machine learning (ML) area. As the term “supervised” suggests, this type of ML is purely data-driven to allow computers to learn patterns from the input data with known outcomes in order to predict the same target outcomes for a different set of data that is previously unseen by the computer.
Define X and y variables
The dataset will be splitted into X (features) and y (target) variables first.
Note: no need to use to_numpy() as there’s a transform step included when using pipeline to create a LR model (also StandardScaler() going to be used). This also applies if using fit_transform() or transform() when not using pipeline - see scikit-learn reference on “transform”.
Prepare training and testing sets
Then the data will be further splitted into separate training and testing sets.
## Random number generator#rng = np.random.RandomState(0) - note: this may produce different result each timeX_train, X_test, y_train, y_test = train_test_split(X, y, test_size =0.2, random_state =50)print('Training set:', X_train.shape, y_train.shape)print('Testing set:', X_test.shape, y_test.shape)
Training set: (4536, 8) (4536,)
Testing set: (1134, 8) (1134,)
chaining preprocessing step with different transformers and estimators in one go where we only have to call fit and predict once on our data
avoiding data leakage from the testing set into the training set by making sure the same set of samples is used to train the transformers and predictors
avoiding missing out on the transform step (note: calling fit() on pipeline is equivalent to calling fit() on each estimator and transform() input data before the next step, plus StandardScaler() is going to be used in the pipeline as well - repeating myself here but this is just a gentle reminder…)
The example below uses Pipeline() to construct a pipeline that takes in a standard scaler to scale data and also a LR estimator, along with some parameters.
## Pipeline:# Ensure prediction output can be read in polars dfset_config(transform_output="polars")# multi_class defaults to 'auto' which selects 'ovr' if the data is binary, or if solver='liblinear'# multi_class is deprecated in version 1.5 and will be removed in 1.7 # this post uses sklearn version 1.5.0params_lr = {# solver for small dataset"solver": "liblinear","random_state": 50}LR = Pipeline(steps=[# Preprocess/scale the dataset (transformer) ("StandardScaler", StandardScaler()), # can add set_output() if preferred# e.g. StandardScaler().set_output(transform="polars")# Create an instance of LR classifier (estimator) ("LogR", LogisticRegression(**params_lr)) ])# can add set_output() if preferred e.g. LR.set_output(transform="polars")LR.fit(X_train, y_train)pred = LR.predict(X_test)LR.score(X_test, y_test)
0.689594356261023
During the pipeline building, I’ve figured out how to integrate set_output() in Polars, and noted that the best use case is to show the feature_names_in_ along with coef_ (scikit-learn reference). The first issue is that the feature names are being generated as “[x0, x1, x2…]”, which is not useful. One of the possible reasons could be because all the molecular features are not in strings (as they’re either i64 or f64), so the feature names are not shown - I’m actually unsure about this but this is just my guess.
One of the other ways I’ve tried is to use ColumnTransformer() within the pipeline (scikit-learn reference - code example folded below) but unfortunately it hasn’t worked as well as expected.
The pipeline above is the final version that works to show molecular feature names with their corresponding coefficients in a polars dataframe output. There are 3 options to add either set_config(transform_output="polars") or set_output(transform_output="polars") with the pipeline code - only really needing one line (and not all 3 - it’ll still work but probably unnecessary to add extra code). I’ve marked all 3 options in the pipeline code above.
Molecular features and coefficients
Next, I’m calling out the LR model used above in the pipeline as we want to get the feature names used for training and predicting along with their corresponding coefficients, and generate a bar chart to show their relationship (reference on plotting directly in Polars using Altair).
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
# Save feature array as dflr_feat = pl.Series(log_reg.feature_names_in_).to_frame("Feature_names")# Explode df due to a list series - e.g. array([[1, 2, 3...]]) and not array([1, 2, 3...])lr_coef = pl.Series(log_reg.coef_).to_frame("Coef").explode("Coef")# Concatenate dfs horizontallydf_feat = pl.concat([lr_feat, lr_coef], how="horizontal")# Using altair to plot feature names vs. coefficients df_feat.plot.bar( x="Coef", # -x = sorting in descending order, x = ascending y=alt.Y("Feature_names").sort("-x"), #color="Feature_names", #will create a legend if used tooltip="Coef",).configure_axis( labelFontSize=15, titleFontSize=15).configure_view( continuousWidth=600, discreteHeight=300)
#RO5 Violations, CXLogP, HBA and HBD all have positive weights or coefficients, when the rest of the molecular features (CXLogD, heavy atoms, polar surface area and QED Weighted) all have the negative coefficients. This is likely the equivalent of using the feature_importances_ in random forest I’m guessing. I’ve sorted the order of coefficients from highest to lowest in the chart.
Another way to get features names is from the pipeline as well but requires a step saving dataframe column names separately as an NumPy array first (scikit-learn reference). The previous way seems to save a bit more time on coding as there’s no need to do this, and also you can retrieve the coefficients of the features at the same time.
One way to get predicted probabilities of the samples in each outcome class (either 0 - not approved or 1 - approved) is via predict_proba() in scikit-learn.
Then we can convert the predicted probabilities into a polars dataframe, along with a statistics summary.
pl.DataFrame(y_mp_pre_proba).describe()
shape: (9, 3)
statistic
column_0
column_1
str
f64
f64
"count"
1134.0
1134.0
"null_count"
0.0
0.0
"mean"
0.486442
0.513558
"std"
0.199652
0.199652
"min"
0.00459
0.044198
"25%"
0.341463
0.360326
"50%"
0.506803
0.493416
"75%"
0.639674
0.658537
"max"
0.955802
0.99541
Pickle LR pipeline
This last part is really for saving the LR pipeline for the next post on evaluating the LR model. I’ve talked a bit more about the security aspect of pickling files in this old post in case anyone’s interested.
# Pickle to save (serialise) the model in working directory (specify path if needed)pickle.dump(LR, open("LR.pkl", "wb")) # "wb" - write binary# Unpickle (de-serialise) the modelLR2 = pickle.load(open("LR.pkl", "rb")) # "rb" - read binary# Use the unpickled model object to make predictionpred2 = LR2.predict(X_test)## Check unpickled model and original model are the same via Python's assertion method#assert np.sum(np.abs(pred2 - pred)) == 0## or alternatively use numpy's allclose()print(np.allclose(pred, pred2)) # note: pred = LR.predict(X_test) from original LR pipeline