{"id":1774,"date":"2024-11-23T15:05:44","date_gmt":"2024-11-23T14:05:44","guid":{"rendered":"https:\/\/lorentzen.ch\/?p=1774"},"modified":"2025-03-02T14:55:05","modified_gmt":"2025-03-02T13:55:05","slug":"effect-plots-in-python-and-r","status":"publish","type":"post","link":"https:\/\/lorentzen.ch\/index.php\/2024\/11\/23\/effect-plots-in-python-and-r\/","title":{"rendered":"Effect Plots in Python and R"},"content":{"rendered":"\n<p>Christian and me did some code magic: Highly effective plots that help to build and inspect any model:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Python:<\/strong> <a href=\"https:\/\/github.com\/lorentzenchr\/model-diagnostics\">https:\/\/github.com\/lorentzenchr\/model-diagnostics<\/a> <br><code>pip install model-diagnostics<\/code><\/li>\n\n\n\n<li><strong>R:<\/strong> <a href=\"https:\/\/github.com\/mayer79\/effectplots\">https:\/\/github.com\/mayer79\/effectplots<\/a><br><code>install.packages(\"effectplots\")<\/code><\/li>\n<\/ul>\n\n\n\n<p>The functionality is best described by its output:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"789\" height=\"790\" src=\"https:\/\/lorentzen.ch\/wp-content\/uploads\/2025\/03\/image.png\" alt=\"\" class=\"wp-image-1802\" srcset=\"https:\/\/lorentzen.ch\/wp-content\/uploads\/2025\/03\/image.png 789w, https:\/\/lorentzen.ch\/wp-content\/uploads\/2025\/03\/image-300x300.png 300w, https:\/\/lorentzen.ch\/wp-content\/uploads\/2025\/03\/image-150x150.png 150w, https:\/\/lorentzen.ch\/wp-content\/uploads\/2025\/03\/image-768x769.png 768w\" sizes=\"auto, (max-width: 789px) 100vw, 789px\" \/><figcaption class=\"wp-element-caption\">Python<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"1024\" src=\"https:\/\/lorentzen.ch\/wp-content\/uploads\/2024\/11\/effectplot_r-1024x1024.png\" alt=\"\" class=\"wp-image-1776\" srcset=\"https:\/\/lorentzen.ch\/wp-content\/uploads\/2024\/11\/effectplot_r-1024x1024.png 1024w, https:\/\/lorentzen.ch\/wp-content\/uploads\/2024\/11\/effectplot_r-300x300.png 300w, https:\/\/lorentzen.ch\/wp-content\/uploads\/2024\/11\/effectplot_r-150x150.png 150w, https:\/\/lorentzen.ch\/wp-content\/uploads\/2024\/11\/effectplot_r-768x768.png 768w, https:\/\/lorentzen.ch\/wp-content\/uploads\/2024\/11\/effectplot_r-1536x1536.png 1536w, https:\/\/lorentzen.ch\/wp-content\/uploads\/2024\/11\/effectplot_r-2048x2048.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">R<\/figcaption><\/figure>\n\n\n\n<p>The plots show different types of feature effects relevant in modeling:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Average observed: Descriptive effect (also interesting without model).<\/li>\n\n\n\n<li>Average predicted: Combined effect of all features. Also called &#8220;M Plot&#8221; (Apley 2020).<\/li>\n\n\n\n<li>Partial dependence: Effect of one feature, keeping other feature values constant (Friedman 2001).<\/li>\n\n\n\n<li>Number of observations or sum of case weights: Feature value distribution.<\/li>\n\n\n\n<li><em>R only: Accumulated local effects, an alternative to partial dependence (Apley 2020).<\/em><\/li>\n<\/ul>\n\n\n\n<p><strong>Both implementations&#8230;<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>are highly efficient<\/strong> thanks to {Polars} in Python and {collapse} in R,  and work on datasets with millions of observations,<\/li>\n\n\n\n<li>support <strong>case weights<\/strong> with all their statistics, ideal in insurance applications,<\/li>\n\n\n\n<li>calculate average residuals (not shown in the plots above),<\/li>\n\n\n\n<li>provide standard deviations\/errors of average observed and bias,<\/li>\n\n\n\n<li>allow to switch to <strong>Plotly<\/strong> for interactive plots, and<\/li>\n\n\n\n<li>are <strong>highly customizable<\/strong> (the R package, e.g., allows to collapse rare levels after calculating statistics via the <code>update()<\/code> method or to sort the features according to main effect importance).<\/li>\n<\/ul>\n\n\n\n<p>In the spirit of our &#8220;Lost In Translation&#8221; series, we provide both high-quality Python and R code. We will use the same data and models as in one of our latest <a href=\"https:\/\/lorentzen.ch\/index.php\/2024\/02\/02\/ml-xai-strong-glm-in-python\/\">posts<\/a> on how to build strong GLMs via ML + XAI.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Example<\/h2>\n\n\n\n<p>Let&#8217;s build a Poisson LightGBM model to explain the claim frequency given six traditional features in a pricing dataset on motor liability claims. 80% of the 1 Mio rows are used for training, the other 20% for evaluation. Hyper-parameters have been slightly tuned (not shown).<\/p>\n\n\n<div class=\"wp-block-ub-tabbed-content wp-block-ub-tabbed-content-holder wp-block-ub-tabbed-content-horizontal-holder-mobile wp-block-ub-tabbed-content-horizontal-holder-tablet\" id=\"ub-tabbed-content-ce6a4c98-9f5d-4fef-8b94-7eea53340699\" style=\"\">\n\t\t\t<div class=\"wp-block-ub-tabbed-content-tab-holder horizontal-tab-width-mobile horizontal-tab-width-tablet\">\n\t\t\t\t<div role=\"tablist\" class=\"wp-block-ub-tabbed-content-tabs-title wp-block-ub-tabbed-content-tabs-title-mobile-horizontal-tab wp-block-ub-tabbed-content-tabs-title-tablet-horizontal-tab\" style=\"justify-content: flex-start; \"><div role=\"tab\" id=\"ub-tabbed-content-ce6a4c98-9f5d-4fef-8b94-7eea53340699-tab-0\" aria-controls=\"ub-tabbed-content-ce6a4c98-9f5d-4fef-8b94-7eea53340699-panel-0\" aria-selected=\"false\" class=\"wp-block-ub-tabbed-content-tab-title-wrap\" style=\"--ub-tabbed-active-title-color: inherit; --ub-tabbed-active-title-background-color: #6d6d6d; text-align: center; \" tabindex=\"-1\">\n\t\t\t\t<div class=\"wp-block-ub-tabbed-content-tab-title\">R<\/div>\n\t\t\t<\/div><div role=\"tab\" id=\"ub-tabbed-content-ce6a4c98-9f5d-4fef-8b94-7eea53340699-tab-1\" aria-controls=\"ub-tabbed-content-ce6a4c98-9f5d-4fef-8b94-7eea53340699-panel-1\" aria-selected=\"true\" class=\"wp-block-ub-tabbed-content-tab-title-wrap active\" style=\"--ub-tabbed-title-background-color: #6d6d6d; --ub-tabbed-active-title-color: inherit; --ub-tabbed-active-title-background-color: #6d6d6d; text-align: center; \" tabindex=\"-1\">\n\t\t\t\t<div class=\"wp-block-ub-tabbed-content-tab-title\">Python<\/div>\n\t\t\t<\/div><\/div>\n\t\t\t<\/div>\n\t\t\t<div class=\"wp-block-ub-tabbed-content-tabs-content\" style=\"\"><div role=\"tabpanel\" class=\"wp-block-ub-tabbed-content-tab-content-wrap ub-hide\" id=\"ub-tabbed-content-ce6a4c98-9f5d-4fef-8b94-7eea53340699-panel-0\" aria-labelledby=\"ub-tabbed-content-ce6a4c98-9f5d-4fef-8b94-7eea53340699-tab-0\" tabindex=\"0\">\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting='{\"showPanel\":true,\"languageLabel\":\"language\",\"fullScreenButton\":true,\"copyButton\":true,\"mode\":\"r\",\"mime\":\"text\/x-rsrc\",\"theme\":\"material\",\"lineNumbers\":false,\"styleActiveLine\":false,\"lineWrapping\":false,\"readOnly\":true,\"fileName\":\"\",\"language\":\"R\",\"maxHeight\":\"400px\",\"modeName\":\"r\"}'>library(OpenML)\nlibrary(lightgbm)\n\ndim(df &lt;- getOMLDataSet(data.id = 45106L)$data)  # 1000000 7\nhead(df)\n\n#   year town driver_age car_weight car_power car_age claim_nb\n# 0 2018    1         51       1760       173       3        0\n# 1 2019    1         41       1760       248       2        0\n# 2 2018    1         25       1240       111       2        0\n# 3 2019    0         40       1010        83       9        0\n# 4 2018    0         43       2180       169       5        0\n# 5 2018    1         45       1170       149       1        1\n\nyvar &lt;- \"claim_nb\"\nxvars &lt;- setdiff(colnames(df), yvar)\n\nix &lt;- 1:800000\ntrain &lt;- df[ix, ]\ntest &lt;- df[-ix, ]\nX_train &lt;- data.matrix(train[xvars])\nX_test &lt;- data.matrix(test[xvars])\n\n# Training, using slightly optimized parameters found via cross-validation\nparams &lt;- list(\n  learning_rate = 0.05,\n  objective = \"poisson\",\n  num_leaves = 7,\n  min_data_in_leaf = 50,\n  min_sum_hessian_in_leaf = 0.001,\n  colsample_bynode = 0.8,\n  bagging_fraction = 0.8,\n  lambda_l1 = 3,\n  lambda_l2 = 5,\n  num_threads = 7\n)\n\nset.seed(1)\n\nfit &lt;- lgb.train(\n  params = params,\n  data = lgb.Dataset(X_train, label = train$claim_nb),\n  nrounds = 300\n)<\/pre><\/div>\n\n<\/div><div role=\"tabpanel\" class=\"wp-block-ub-tabbed-content-tab-content-wrap active\" id=\"ub-tabbed-content-ce6a4c98-9f5d-4fef-8b94-7eea53340699-panel-1\" aria-labelledby=\"ub-tabbed-content-ce6a4c98-9f5d-4fef-8b94-7eea53340699-tab-1\" tabindex=\"0\">\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting='{\"showPanel\":true,\"languageLabel\":\"language\",\"fullScreenButton\":true,\"copyButton\":true,\"mode\":\"python\",\"mime\":\"text\/x-python\",\"theme\":\"material\",\"lineNumbers\":false,\"styleActiveLine\":false,\"lineWrapping\":false,\"readOnly\":true,\"fileName\":\"\",\"language\":\"Python\",\"maxHeight\":\"400px\",\"modeName\":\"python\"}'>import matplotlib.pyplot as plt\nfrom lightgbm import LGBMRegressor\nfrom sklearn.datasets import fetch_openml\n\ndf = fetch_openml(data_id=45106, parser=\"pandas\").frame\ndf.head()\n\n#   year town driver_age car_weight car_power car_age claim_nb\n# 0 2018    1         51       1760       173       3        0\n# 1 2019    1         41       1760       248       2        0\n# 2 2018    1         25       1240       111       2        0\n# 3 2019    0         40       1010        83       9        0\n# 4 2018    0         43       2180       169       5        0\n# 5 2018    1         45       1170       149       1        1\n\n# Train model on 80% of the data\ny = df.pop(\"claim_nb\")\nn_train = 800_000\nX_train, y_train = df.iloc[:n_train], y.iloc[:n_train]\nX_test, y_test = df.iloc[n_train:], y.iloc[n_train:]\n\nparams = {\n    \"learning_rate\": 0.05,\n    \"objective\": \"poisson\",\n    \"num_leaves\": 7,\n    \"min_child_samples\": 50,\n    \"min_child_weight\": 0.001,\n    \"colsample_bynode\": 0.8,\n    \"subsample\": 0.8,\n    \"reg_alpha\": 3,\n    \"reg_lambda\": 5,\n    \"verbose\": -1,\n}\n\nmodel = LGBMRegressor(n_estimators=300, **params, random_state=1)\nmodel.fit(X_train, y_train)<\/pre><\/div>\n\n<\/div><\/div>\n\t\t<\/div>\n\n\n<h3 class=\"wp-block-heading\"><\/h3>\n\n\n\n<p>Let&#8217;s inspect the (main effects) of the model on the test data.<\/p>\n\n\n<div class=\"wp-block-ub-tabbed-content wp-block-ub-tabbed-content-holder wp-block-ub-tabbed-content-horizontal-holder-mobile wp-block-ub-tabbed-content-horizontal-holder-tablet\" id=\"ub-tabbed-content-d4633134-5ea3-43a4-8c83-e6927bb3335b\" style=\"\">\n\t\t\t<div class=\"wp-block-ub-tabbed-content-tab-holder horizontal-tab-width-mobile horizontal-tab-width-tablet\">\n\t\t\t\t<div role=\"tablist\" class=\"wp-block-ub-tabbed-content-tabs-title wp-block-ub-tabbed-content-tabs-title-mobile-horizontal-tab wp-block-ub-tabbed-content-tabs-title-tablet-horizontal-tab\" style=\"justify-content: flex-start; \"><div role=\"tab\" id=\"ub-tabbed-content-d4633134-5ea3-43a4-8c83-e6927bb3335b-tab-0\" aria-controls=\"ub-tabbed-content-d4633134-5ea3-43a4-8c83-e6927bb3335b-panel-0\" aria-selected=\"false\" class=\"wp-block-ub-tabbed-content-tab-title-wrap\" style=\"--ub-tabbed-active-title-color: inherit; --ub-tabbed-active-title-background-color: #6d6d6d; text-align: center; \" tabindex=\"-1\">\n\t\t\t\t<div class=\"wp-block-ub-tabbed-content-tab-title\">R<\/div>\n\t\t\t<\/div><div role=\"tab\" id=\"ub-tabbed-content-d4633134-5ea3-43a4-8c83-e6927bb3335b-tab-1\" aria-controls=\"ub-tabbed-content-d4633134-5ea3-43a4-8c83-e6927bb3335b-panel-1\" aria-selected=\"true\" class=\"wp-block-ub-tabbed-content-tab-title-wrap active\" style=\"--ub-tabbed-title-background-color: #6d6d6d; --ub-tabbed-active-title-color: inherit; --ub-tabbed-active-title-background-color: #6d6d6d; text-align: center; \" tabindex=\"-1\">\n\t\t\t\t<div class=\"wp-block-ub-tabbed-content-tab-title\">Python<\/div>\n\t\t\t<\/div><\/div>\n\t\t\t<\/div>\n\t\t\t<div class=\"wp-block-ub-tabbed-content-tabs-content\" style=\"\"><div role=\"tabpanel\" class=\"wp-block-ub-tabbed-content-tab-content-wrap ub-hide\" id=\"ub-tabbed-content-d4633134-5ea3-43a4-8c83-e6927bb3335b-panel-0\" aria-labelledby=\"ub-tabbed-content-d4633134-5ea3-43a4-8c83-e6927bb3335b-tab-0\" tabindex=\"0\">\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting='{\"showPanel\":true,\"languageLabel\":\"language\",\"fullScreenButton\":true,\"copyButton\":true,\"mode\":\"r\",\"mime\":\"text\/x-rsrc\",\"theme\":\"material\",\"lineNumbers\":false,\"styleActiveLine\":false,\"lineWrapping\":false,\"readOnly\":true,\"fileName\":\"\",\"language\":\"R\",\"maxHeight\":\"400px\",\"modeName\":\"r\"}'>library(effectplots)\n\n# 0.3 s\nfeature_effects(fit, v = xvars, data = X_test, y = test$claim_nb) |&gt;\n  plot(share_y = \"all\")<\/pre><\/div>\n\n<\/div><div role=\"tabpanel\" class=\"wp-block-ub-tabbed-content-tab-content-wrap active\" id=\"ub-tabbed-content-d4633134-5ea3-43a4-8c83-e6927bb3335b-panel-1\" aria-labelledby=\"ub-tabbed-content-d4633134-5ea3-43a4-8c83-e6927bb3335b-tab-1\" tabindex=\"0\">\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting='{\"showPanel\":true,\"languageLabel\":\"language\",\"fullScreenButton\":true,\"copyButton\":true,\"mode\":\"python\",\"mime\":\"text\/x-python\",\"theme\":\"material\",\"lineNumbers\":false,\"styleActiveLine\":false,\"lineWrapping\":false,\"readOnly\":true,\"fileName\":\"\",\"language\":\"Python\",\"maxHeight\":\"400px\",\"modeName\":\"python\"}'>from model_diagnostics.calibration import plot_marginal\n\nfig, axes = plt.subplots(3, 2, figsize=(8, 8), sharey=True, layout=\"tight\")\n\n# 2.3 s\nfor i, (feat, ax) in enumerate(zip(X_test.columns, axes.flatten())):\n    plot_marginal(\n        y_obs=y_test,\n        y_pred=model.predict(X_test),\n        X=X_test,\n        feature_name=feat,\n        predict_function=model.predict,\n        ax=ax,\n    )\n    ax.set_title(feat)\n    if i != 1:\n        ax.legend().remove()<\/pre><\/div>\n\n<\/div><\/div>\n\t\t<\/div>\n\n\n<p>The output can be seen at the beginning of this blog post.<\/p>\n\n\n\n<p><strong>Here some model insights:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Average predictions closely match observed frequencies. No clear bias is visible.<\/li>\n\n\n\n<li>Partial dependence shows that the year and the car weight almost have no impact (regarding their main effects), while the <code>driver_age<\/code> and <code>car_power<\/code> effects seem strongest. The shared y axes help to assess these.<\/li>\n\n\n\n<li>Except for <code>car_weight<\/code>, the partial dependence curve closely follows the average predictions. This means that the model effect seems to really come from the feature on the x axis, and not of some correlated other feature (as, e.g., with <code>car_weight<\/code> which is actually strongly correlated with <code>car_power<\/code>).<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Final words<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Inspecting models has become much relaxed with above functions.<\/li>\n\n\n\n<li>The packages used offer much more functionality. Try them out! Or we will show them in later posts ;).<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">References<a href=\"https:\/\/github.com\/mayer79\/effectplots#references\"><\/a><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Apley, Daniel W., and Jingyu Zhu. 2020.&nbsp;<em>Visualizing the Effects of Predictor Variables in Black Box Supervised Learning Models.<\/em>&nbsp;Journal of the Royal Statistical Society Series B: Statistical Methodology, 82 (4): 1059\u20131086. doi:10.1111\/rssb.12377.<\/li>\n\n\n\n<li>Friedman, Jerome H. 2001.&nbsp;<em>Greedy Function Approximation: A Gradient Boosting Machine.<\/em>&nbsp;Annals of Statistics 29 (5): 1189\u20131232. doi:10.1214\/aos\/1013203451.<\/li>\n<\/ol>\n\n\n\n<p><a href=\"https:\/\/github.com\/lorentzenchr\/notebooks\/blob\/master\/blogposts\/2024-11-23%20effectplots.R\">R script<\/a> , <a href=\"https:\/\/github.com\/lorentzenchr\/notebooks\/blob\/master\/blogposts\/2024-11-23_effectplots.ipynb\">Python notebook<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post introduces new Python and R functionality how to get a quick summary of any model.<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[16,17,9],"tags":[10,6,5],"class_list":["post-1774","post","type-post","status-publish","format-standard","hentry","category-machine-learning","category-programming","category-statistics","tag-lost-in-translation","tag-python","tag-r"],"featured_image_src":null,"author_info":{"display_name":"Michael Mayer","author_link":"https:\/\/lorentzen.ch\/index.php\/author\/michael\/"},"_links":{"self":[{"href":"https:\/\/lorentzen.ch\/index.php\/wp-json\/wp\/v2\/posts\/1774","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/lorentzen.ch\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/lorentzen.ch\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/lorentzen.ch\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/lorentzen.ch\/index.php\/wp-json\/wp\/v2\/comments?post=1774"}],"version-history":[{"count":10,"href":"https:\/\/lorentzen.ch\/index.php\/wp-json\/wp\/v2\/posts\/1774\/revisions"}],"predecessor-version":[{"id":1875,"href":"https:\/\/lorentzen.ch\/index.php\/wp-json\/wp\/v2\/posts\/1774\/revisions\/1875"}],"wp:attachment":[{"href":"https:\/\/lorentzen.ch\/index.php\/wp-json\/wp\/v2\/media?parent=1774"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/lorentzen.ch\/index.php\/wp-json\/wp\/v2\/categories?post=1774"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/lorentzen.ch\/index.php\/wp-json\/wp\/v2\/tags?post=1774"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}