jupyter | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
Before diving into the receiver operating characteristic (ROC) curve, we will look at two plots that will give some context to the thresholds mechanism behind the ROC and PR curves.
In the histogram, we observe that the score spread such that most of the positive labels are binned near 1, and a lot of the negative labels are close to 0. When we set a threshold on the score, all of the bins to its left will be classified as 0's, and everything to the right will be 1's. There are obviously a few outliers, such as negative samples that our model gave a high score, and positive samples with a low score. If we set a threshold right in the middle, those outliers will respectively become false positives and false negatives.
As we adjust thresholds, the number of positive positives will increase or decrease, and at the same time the number of true positives will also change; this is shown in the second plot. As you can see, the model seems to perform fairly well, because the true positive rate decreases slowly, whereas the false positive rate decreases sharply as we increase the threshold. Those two lines each represent a dimension of the ROC curve.
importplotly.expressaspximportpandasaspdfromsklearn.linear_modelimportLogisticRegressionfromsklearn.metricsimportroc_curve, aucfromsklearn.datasetsimportmake_classificationX, y=make_classification(n_samples=500, random_state=0) model=LogisticRegression() model.fit(X, y) y_score=model.predict_proba(X)[:, 1] fpr, tpr, thresholds=roc_curve(y, y_score) # The histogram of scores compared to true labelsfig_hist=px.histogram( x=y_score, color=y, nbins=50, labels=dict(color='True Labels', x='Score') ) fig_hist.show() # Evaluating model performance at various thresholdsdf=pd.DataFrame({ 'False Positive Rate': fpr, 'True Positive Rate': tpr }, index=thresholds) df.index.name="Thresholds"df.columns.name="Rate"fig_thresh=px.line( df, title='TPR and FPR at every threshold', width=700, height=500 ) fig_thresh.update_yaxes(scaleanchor="x", scaleratio=1) fig_thresh.update_xaxes(range=[0, 1], constrain='domain') fig_thresh.show()
Notice how this ROC curve looks similar to the True Positive Rate curve from the previous plot. This is because they are the same curve, except the x-axis consists of increasing values of FPR instead of threshold, which is why the line is flipped and distorted.
We also display the area under the ROC curve (ROC AUC), which is fairly high, thus consistent with our interpretation of the previous plots.
importplotly.expressaspxfromsklearn.linear_modelimportLogisticRegressionfromsklearn.metricsimportroc_curve, aucfromsklearn.datasetsimportmake_classificationX, y=make_classification(n_samples=500, random_state=0) model=LogisticRegression() model.fit(X, y) y_score=model.predict_proba(X)[:, 1] fpr, tpr, thresholds=roc_curve(y, y_score) fig=px.area( x=fpr, y=tpr, title=f'ROC Curve (AUC={auc(fpr, tpr):.4f})', labels=dict(x='False Positive Rate', y='True Positive Rate'), width=700, height=500 ) fig.add_shape( type='line', line=dict(dash='dash'), x0=0, x1=1, y0=0, y1=1 ) fig.update_yaxes(scaleanchor="x", scaleratio=1) fig.update_xaxes(constrain='domain') fig.show()
Dash is the best way to build analytical apps in Python using Plotly figures. To run the app below, run pip install dash
, click "Download" to get the code and run python app.py
.
Get started with the official Dash docs and learn how to effortlessly style & deploy apps like this with Dash Enterprise.
fromIPython.displayimportIFramesnippet_url='https://python-docs-dash-snippets.herokuapp.com/python-docs-dash-snippets/'IFrame(snippet_url+'roc-and-pr-curves', width='100%', height=1200)
Sign up for Dash Club → Free cheat sheets plus updates from Chris Parmer and Adam Schroeder delivered to your inbox every two months. Includes tips and tricks, community apps, and deep dives into the Dash architecture. Join now.
When you have more than 2 classes, you will need to plot the ROC curve for each class separately. Make sure that you use a one-versus-rest model, or make sure that your problem has a multi-label format; otherwise, your ROC curve might not return the expected results.
importplotly.graph_objectsasgoimportplotly.expressaspximportnumpyasnpimportpandasaspdfromsklearn.linear_modelimportLogisticRegressionfromsklearn.metricsimportroc_curve, roc_auc_scorenp.random.seed(0) # Artificially add noise to make task harderdf=px.data.iris() samples=df.species.sample(n=50, random_state=0) np.random.shuffle(samples.values) df.loc[samples.index, 'species'] =samples.values# Define the inputs and outputsX=df.drop(columns=['species', 'species_id']) y=df['species'] # Fit the modelmodel=LogisticRegression(max_iter=200) model.fit(X, y) y_scores=model.predict_proba(X) # One hot encode the labels in order to plot themy_onehot=pd.get_dummies(y, columns=model.classes_) # Create an empty figure, and iteratively add new lines# every time we compute a new classfig=go.Figure() fig.add_shape( type='line', line=dict(dash='dash'), x0=0, x1=1, y0=0, y1=1 ) foriinrange(y_scores.shape[1]): y_true=y_onehot.iloc[:, i] y_score=y_scores[:, i] fpr, tpr, _=roc_curve(y_true, y_score) auc_score=roc_auc_score(y_true, y_score) name=f"{y_onehot.columns[i]} (AUC={auc_score:.2f})"fig.add_trace(go.Scatter(x=fpr, y=tpr, name=name, mode='lines')) fig.update_layout( xaxis=dict( title=dict( text='False Positive Rate' ), constrain='domain' ), yaxis=dict( title=dict( text='True Positive Rate' ), scaleanchor='x', scaleratio=1 ), width=700, height=500 ) fig.show()
Plotting the PR curve is very similar to plotting the ROC curve. The following examples are slightly modified from the previous examples:
importplotly.expressaspxfromsklearn.linear_modelimportLogisticRegressionfromsklearn.metricsimportprecision_recall_curve, aucfromsklearn.datasetsimportmake_classificationX, y=make_classification(n_samples=500, random_state=0) model=LogisticRegression() model.fit(X, y) y_score=model.predict_proba(X)[:, 1] precision, recall, thresholds=precision_recall_curve(y, y_score) fig=px.area( x=recall, y=precision, title=f'Precision-Recall Curve (AUC={auc(recall, precision):.4f})', labels=dict(x='Recall', y='Precision'), width=700, height=500 ) fig.add_shape( type='line', line=dict(dash='dash'), x0=0, x1=1, y0=1, y1=0 ) fig.update_yaxes(scaleanchor="x", scaleratio=1) fig.update_xaxes(constrain='domain') fig.show()
In this example, we use the average precision metric, which is an alternative scoring method to the area under the PR curve.
importplotly.graph_objectsasgoimportplotly.expressaspximportnumpyasnpimportpandasaspdfromsklearn.linear_modelimportLogisticRegressionfromsklearn.metricsimportprecision_recall_curve, average_precision_scorenp.random.seed(0) # Artificially add noise to make task harderdf=px.data.iris() samples=df.species.sample(n=30, random_state=0) np.random.shuffle(samples.values) df.loc[samples.index, 'species'] =samples.values# Define the inputs and outputsX=df.drop(columns=['species', 'species_id']) y=df['species'] y_onehot=pd.get_dummies(y, columns=model.classes_) # Fit the modelmodel=LogisticRegression(max_iter=200) model.fit(X, y) y_scores=model.predict_proba(X) # Create an empty figure, and iteratively add new lines# every time we compute a new classfig=go.Figure() fig.add_shape( type='line', line=dict(dash='dash'), x0=0, x1=1, y0=1, y1=0 ) foriinrange(y_scores.shape[1]): y_true=y_onehot.iloc[:, i] y_score=y_scores[:, i] precision, recall, _=precision_recall_curve(y_true, y_score) auc_score=average_precision_score(y_true, y_score) name=f"{y_onehot.columns[i]} (AP={auc_score:.2f})"fig.add_trace(go.Scatter(x=recall, y=precision, name=name, mode='lines')) fig.update_layout( xaxis=dict( title=dict( text='Recall' ), constrain='domain' ), yaxis=dict( title=dict( text='Precision' ), scaleanchor='x', scaleratio=1 ), width=700, height=500 ) fig.show()
Learn more about px
, px.area
, px.hist
: