<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Tan Nian Wei on Medium]]></title>
        <description><![CDATA[Stories by Tan Nian Wei on Medium]]></description>
        <link>https://medium.com/@tnwei?source=rss-7c0fe8aac76e------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*iaQEJpjI1lTrt_7TBS_ktA.jpeg</url>
            <title>Stories by Tan Nian Wei on Medium</title>
            <link>https://medium.com/@tnwei?source=rss-7c0fe8aac76e------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 10:24:48 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@tnwei/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Read Jupyter notebooks from the command line]]></title>
            <link>https://medium.com/@tnwei/read-jupyter-notebooks-from-the-command-line-5659886f0f3d?source=rss-7c0fe8aac76e------2</link>
            <guid isPermaLink="false">https://medium.com/p/5659886f0f3d</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[jupyter]]></category>
            <dc:creator><![CDATA[Tan Nian Wei]]></dc:creator>
            <pubDate>Tue, 25 Apr 2023 16:22:35 GMT</pubDate>
            <atom:updated>2023-04-25T16:22:35.643Z</atom:updated>
            <content:encoded><![CDATA[<p>Jupyter notebooks enable data scientists to explore data and prototype solutions just like walking. Put your left foot forward (code), then your right foot (plot), then your left foot again (code), and rinse and repeat. This is in no small part thanks to the Javascript-based GUI that interfaces with Python kernels under the hood, to power the immediate interactivity that underlies the modern data science workflow.</p><p>This immediate interactivity is sorely felt when it is missing.</p><h3>Displaying textual content in Jupyter notebooks should be fast</h3><p>Once I was going through code submitted by participants for a hackathon for archival purposes. I didn’t need to run any notebooks; just needed to skim through them as part of the process. There was one particular notebook that was full of graphs, generated by a couple of loops. Did you know that images in code cell outputs are stored as base64 encoded strings in the notebook itself? This particular notebook weighted in at &gt;60 MBs, and it froze my Jupyter Lab for a minute or two upon opening it.</p><p>Although I don’t run into monster notebooks often, I run into similar productivity snags in my day to day frequent enough that I think about it. Sometimes I just want to quickly skim a notebook to refresh my memory / to look for something I forgot without intending to run the notebook¹.</p><p>Jupyter notebooks tend to take a couple of seconds to load in Jupyter Lab. It would be nice if I can flip through content-heavy Jupyter notebooks with the same responsiveness of flipping through pages on a physical notebook. Jupyter notebooks are just specially formatted JSON! I shouldn’t need to start a Jupyter server if all I wanted to do is skim it quickly. Reading and displaying a Jupyter notebook JSON should be fast if we just opt to put plain text² in front of eyeballs.</p><h3>Why the command line?</h3><p>I ended up as a heavy command line user after getting used to doing everything on my work server over SSH. Plus, I already use <a href="https://github.com/charmbracelet/glow">charmbracelet/glow</a> to render markdown in CLI, something similar will work just fine for me.</p><h3>Low hanging fruit: someone has implemented it!</h3><p>After studying the format of a Jupyter notebook (<a href="https://nbformat.readthedocs.io/en/latest/">link to nbformat docs</a>), I set out to find libraries for building TUI apps in Python (most comfortable language). I hit the jackpot when I came across <a href="https://github.com/Textualize/rich-cli">textualize/rich-cli</a>, which straight up can pretty print notebooks to terminal! It was as simple as pip install rich-cli, then rich notebook.ipynb.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/711/1*s9Je0lUpnGdf9LoVfI4FOA.jpeg" /><figcaption>I wish there are more packages that just magically exist when I want to do something.</figcaption></figure><p>Tried it out, love how it looks. But this story has not come to an end just yet. When loading large notebooks, there was a perceptible lag of a few seconds before the rendered output shows up. Since we’re just rendering text here, I figured there’s a bottleneck somewhere that can be removed to make the output show up almost instantly.</p><h3>A minor tweak to fully scratch my itch</h3><p>After some tinkering, I found that the lag came from rich-cli preparing all notebook cells for rendering before dumping them to STDOUT all at once. Modifying the logic to render each cell directly before processing the next one is doable, but this breaks the built-in pagination: the pager in the underlying API (<a href="https://github.com/Textualize/rich/">textualize/rich</a>) will display each cell in their own pager instance if I do that. As a workaround, I used subprocess to pipe to less as a pager instead. Because of <a href="https://stackoverflow.com/a/9834118">how bash pipe works</a>, any output received is immediately flushed to STDOUT. Thus, regardless of notebook size, the first few cells are rendered in terminal almost instantly. Bingo!</p><p>Now that the logic works as intended, I packaged the code as nbread and put it on github (<a href="https://github.com/tnwei/nbread">tnwei/nbread</a>), so that I can easily install it as a standalone command-line application with <a href="https://github.com/pypa/pipx">pipx</a>.</p><h3>From good to awesome: integration with ranger file manager</h3><p>I happen to be a happy user of <a href="https://github.com/ranger/ranger">ranger</a>, a command-line file manager that comes with a file preview pane. By default, Jupyter notebooks show up simply as raw JSON in the preview, and is opened as a text file when selected. Given the work done above, I saw how I can improve my user experience by configuring integration with nbread: I won’t even need to launch nbread manually; I can just open the notebook I want by finger-dancing on arrow keys!</p><p>Just had to make the following changes:</p><p>Modify handle_extension() in ~/.config/ranger/scope.sh for file preview:</p><pre>case &quot;$extension&quot; in<br>    ipynb)<br>    ¦   # Jupyter notebook previewer<br>    ¦   nbread &quot;${FILE_PATH}&quot; &amp;&amp; { dump | trim; exit 5; } || exit 2;;</pre><p>And adding the following to ~/.config/ranger/rifle.conf to launch nbread when a Jupyter notebook is selected:</p><pre># Jupyter notebooks<br>ext ipynb = nbread &quot;$1&quot; --pager</pre><h3>Conclusion and thoughts</h3><p>I ended up using the outcome of this little side project almost every day. It just works and doesn’t take much conscious thought to use. Here’s what using nbread in ranger looks like:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pb-KT0kf8DH0lsbKfRX-ig.png" /><figcaption>See full animation at <a href="https://asciinema.org/a/548838">https://asciinema.org/a/548838</a></figcaption></figure><p>Certainly, there are limitations: none of the rich media content like images and interactive widgets are visible (see footnote 2) . However, as a preview tool, it works more than well enough for my purposes and successfully scratched my itch. So I’m happy with it.</p><p>(Original blogpost: <a href="https://tnwei.github.io/posts/preview-jupyter-notebooks-cli/">Read Jupyter notebooks from the command line</a>)</p><ol><li>*GPT it? Hmmm</li><li>What about the rich media formatting, you ask? One, typically I’m looking for textual content instead of rich content when skimming through notebooks: code or notes written in comments / markdown cells. And two, dealing with rich content will make this little side project more complex by a few orders of magnitude. I personally don’t think putting a lot of effort here is worth it.</li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5659886f0f3d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[APGCE Geohack 2022 Part 1: Design Intent]]></title>
            <link>https://medium.com/@tnwei/apgce-geohack-2022-part-1-design-intent-1b398d7b8dd0?source=rss-7c0fe8aac76e------2</link>
            <guid isPermaLink="false">https://medium.com/p/1b398d7b8dd0</guid>
            <category><![CDATA[hackathons]]></category>
            <category><![CDATA[energy]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[geoscience]]></category>
            <dc:creator><![CDATA[Tan Nian Wei]]></dc:creator>
            <pubDate>Wed, 02 Nov 2022 12:30:35 GMT</pubDate>
            <atom:updated>2022-11-02T12:30:35.590Z</atom:updated>
            <content:encoded><![CDATA[<p>How we set up the heart of our upcoming subsurface-themed hackathon</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UJ2hlsH8MuSdkj0R5L3SKw.jpeg" /></figure><p>We hosted the first Geohack a few years ago in 2019, as a side dish to the APGCE 2019 conference. Ten teams of five came together to build solutions within the intersection of machine learning and exploration geoscience. As APGCE 2022 will be held physically in Kuala Lumpur again this year, we jumped at the chance to organize Geohack once more.</p><p>As we count down one month to the event, details are being hammered out like steel in a forge. Although we are currently in the thick of things, I thought it would be a good idea to take advantage of present-day clarity and document our efforts to serve as reference for posterity. In this post, we’ll talk about how we intend to set up the heart of the hackathon: the data, the challenges, and scoring.</p><p>Disclaimer: if you are a prospective participant, take all this information with a grain of salt; things are subject to change as they are.</p><h4>Data selection</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*J_71mer5PHdo1pPv" /><figcaption>Photo by <a href="https://unsplash.com/@swimstaralex?utm_source=medium&amp;utm_medium=referral">Alexander Sinn</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>We want the hackathon to be unique. Thus, using data from active Malaysian oilfields in production came to mind. How many subsurface hackathons in the world let you work on data that still has commercial value?</p><p>As great as that sounds, permission to use this live data comes with strict stipulations. In order to comply with Malaysian data export regulations, we would need to ensure participants can only access the data during the event itself. But that’s just the easy part. The hard part is to ensure the participants no longer have access to the data after the event. Although it is possible to monitor data transfer throughout the event (don’t ask how), the team isn’t too keen to do that. We would know; we’ve done this in the last Geohack.</p><p>The alternative would be to use publicly available oil and gas datasets. Our lives would indeed be easier, but with the scant few datasets available out there, we have the impression that these datasets will already have been utilized for some hackathon or project one way or another.</p><p>Eventually, the thought came to us: why not both? Kaggle, the online machine learning competition platform, runs a dual scoring system where submitted models are scored on a public and private leaderboard. The public leaderboard is based on public validation data that is accessible by participants, while the private leaderboard (which decides the final ranking) is based on private validation data that is not shown to participants. We plan to emulate the concept in spirit: repurpose existing public oil and gas datasets for the hackathon, while allowing participants to run their final solution on the Malaysian field data in a controlled environment. We plan to use Dataiku to serve as a virtual data room of sorts.</p><p>We’re still tinkering with the interplay between public and private data when it comes to the challenges. Speaking of which …</p><h4>Challenges</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dDIO3LTJyOXvO6m7TuOweg.jpeg" /></figure><p>For challenge design, we decided to use a bottom-up approach by listing all possible challenges we can recall / think of, then zoomed in on a select few as a group. At the moment, we narrowed it down to two well-specific challenges and two seismic-specific challenges. We made a conscious decision to limit the number of challenges for two reasons: (i) to focus our data prep efforts better, and (ii) too many challenges makes it difficult to balance them for scoring. We hope that the challenges will be able to cater to teams of varying skillsets; ultimately we just want everyone to have fun at Geohack.</p><p>Speaking of scoring …</p><h4>Scoring</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pCj3-XehTDBkQRCWFfQqGg.jpeg" /></figure><p>I know we mentioned Kaggle earlier, but we don’t plan to score the hackathon like a Kaggle competition. We want Geohack to be open and welcoming to everybody regardless of skill level; making it a score race will defeat the purpose of the event. Instead, participants will be asked to present their solutions via pitching, preferably with a demo. The scoring criteria will take into account creativity, execution, impact, and presentation. No surprises here; our scoring will resemble a standard hackathon.</p><h4>Wrapping up</h4><p>The above information is as much as we can reveal at this point in time. We’ll share more as the events approaches; hope you enjoyed the peek behind the scenes! If you are interested, the event page and signup link can be found at <a href="https://icep.com.my/apgce/geohack">https://icep.com.my/apgce/geohack</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1b398d7b8dd0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Explaining negative R-squared]]></title>
            <link>https://medium.com/data-science/explaining-negative-r-squared-17894ca26321?source=rss-7c0fe8aac76e------2</link>
            <guid isPermaLink="false">https://medium.com/p/17894ca26321</guid>
            <category><![CDATA[statistics]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[regression]]></category>
            <dc:creator><![CDATA[Tan Nian Wei]]></dc:creator>
            <pubDate>Thu, 09 Jun 2022 13:04:08 GMT</pubDate>
            <atom:updated>2022-06-09T13:04:08.511Z</atom:updated>
            <content:encoded><![CDATA[<p>When I first started out doing machine learning, I learnt that:</p><ul><li>R² is the coefficient of determination, a measure of how well is the data explained by the fitted model,</li><li>R² is the square of the coefficient of correlation, R,</li><li>R is a quantity that ranges from 0 to 1</li></ul><p>Therefore, R² should also range from 0 to 1.</p><p>Colour me surprised when the `r2_score` <a href="https://scikit-learn.org/stable/modules/generated/sklearn.metrics.r2_score.html">implementation in sklearn</a> returned negative scores. What gives?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/1*1CgRyoAG8IGoHaIb311nhA.jpeg" /><figcaption>If you glossed over the math by instinct, this meme is for you.</figcaption></figure><blockquote>This article is adapted from my <a href="https://tnwei.github.io/posts/negative-r2/">original blogpost here</a></blockquote><h4>The answer lies in the definition</h4><p>R² is defined upon the basis that the total sum of squares of a fitted model is equal to the explained sum of squares plus the residual sum of squares, or:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/557/1*dArha-uzww1dnV8QRtFoBw.jpeg" /></figure><p>where:</p><ul><li>Total sum of squares (SS_tot​) represent the total variation in data, measured by the sum of squares of the difference between expected and actual values,</li><li>Explained sum of squares (SS_exp​) represent the variation in data explained by the fitted model, and</li><li>Residual sum of squares (SS_res​) represent variation in data that is not explained by the fitted model.</li></ul><p>R² itself is defined as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/471/1*8icqaOJd9lQGmrdZmLeqoA.jpeg" /></figure><p>Given these definitions, note that negative R² is only possible when the residual sum of squares (SS_res) exceeds the total sum of squares (SS_tot). As this is not mathematically possible, it can only mean that the explained sum of squares and residual sum of squares no longer add up to equal the total sum of squares. In other words, the equality in Equation 1 does not appear [1] to be true.</p><p>How can this be?</p><h4>Because we evaluate models separately on train and test data</h4><p>Following the above definitions, SS_tot can be calculated using just the data itself, while SS_res depends both on model predictions and the data. While we can use any arbitrary model to generate the predictions for scoring, we need to realize that the aforementioned equality is defined for <em>models trained on the same data</em>. Therefore, it doesn’t necessarily hold true when we use test data to evaluate models built on train data! There is no guarantee that the differences between a foreign model’s predictions and the data is smaller than the variation within the data itself.</p><p>We can demonstrate this empirically. The code below fits a couple of linear regression models on randomly generated data:</p><pre>from sklearn.model_selection import train_test_split<br>from sklearn.linear_model import LinearRegression<br>from sklearn.metrics import r2_score<br>import numpy as np</pre><pre>for _ in range(20):<br>    data = np.random.normal(size=(200, 10))</pre><pre>    X_train = data[:160, :-1]<br>    X_test = data[160:, :-1]<br>    y_train = data[:160, -1]<br>    y_test = data[160:, -1]</pre><pre>    lr = LinearRegression()<br>    lr.fit(X_train, y_train)</pre><pre>    y_train_pred = lr.predict(X_train)<br>    y_test_pred = lr.predict(X_test)</pre><pre>    train_score = r2_score(y_train, y_train_pred)<br>    test_score = r2_score(y_test, y_test_pred)<br>    print(f&quot;Train R2: {train_score:.3f}, Test R2: {test_score:.3f}&quot;)</pre><p>Try as we might, the R² never drops below zero when the models are evaluated on train data. Here’s what I got in STDOUT:</p><pre>Train R2: 0.079, Test R2: -0.059<br>Train R2: 0.019, Test R2: -0.046<br>Train R2: 0.084, Test R2: -0.060<br>Train R2: 0.020, Test R2: -0.083<br>Train R2: 0.065, Test R2: -0.145<br>Train R2: 0.022, Test R2: 0.032<br>Train R2: 0.048, Test R2: 0.107<br>Train R2: 0.076, Test R2: -0.031<br>Train R2: 0.029, Test R2: 0.006<br>Train R2: 0.069, Test R2: -0.150<br>Train R2: 0.064, Test R2: -0.150<br>Train R2: 0.053, Test R2: 0.096<br>Train R2: 0.062, Test R2: 0.022<br>Train R2: 0.063, Test R2: 0.008<br>Train R2: 0.059, Test R2: -0.061<br>Train R2: 0.076, Test R2: -0.191<br>Train R2: 0.049, Test R2: 0.099<br>Train R2: 0.040, Test R2: -0.012<br>Train R2: 0.096, Test R2: -0.373<br>Train R2: 0.073, Test R2: 0.088</pre><h4>So … what about R² being the square of correlation?</h4><p>It appears that R² = R * R only under limited circumstances. Quoting the paragraph below from the <a href="https://en.wikipedia.org/wiki/Coefficient_of_determination">relevant Wikipedia page</a>:</p><blockquote>There are several definitions of R² that are only sometimes equivalent. One class of such cases includes that of simple linear regression where r² is used instead of R². When only an intercept is included, then r² is simply the square of the sample correlation coefficient (i.e., r) between the observed outcomes and the observed predictor values. If additional regressors are included, R² is the square of the coefficient of multiple correlation. In both such cases, the coefficient of determination normally ranges from 0 to 1.</blockquote><p>In short, R² is only the square of correlation if we happen to be (1) using linear regression models, and (2) are evaluating them on the same data they are fitted (as established previously).</p><h4>On the liberal use of R² outside the context of linear regression</h4><p>The quoted Wikipedia paragraph lines up with my observation flipping through statistical texts: R² is almost always introduced within the context of linear regression. That being said, the formulation of R² makes it universally defined for any arbitrary predictive model, regardless of statistical basis. It is used liberally by data scientists in regression tasks, and is even the default metric for regression models in sklearn. Is it right for us to use R² so freely outside its original context?</p><p>Honestly, I don’t know. On one hand, it clearly has a lot of utility as a metric, which led to its widespread adoption by data scientists in the first place. On the other hand, you can find <a href="https://stats.stackexchange.com/questions/547863/heres-why-you-can-hopefully-use-r2-for-non-linear-models-why-not">discussions</a> <a href="https://blog.minitab.com/en/adventures-in-statistics-2/why-is-there-no-r-squared-for-nonlinear-regression">like</a> <a href="https://statisticsbyjim.com/regression/r-squared-invalid-nonlinear-regression/">these</a> online that caution against using R² for non-linear regression. It does seem to me that from a statistics perspective, it is important for R² to be calculated under the right conditions such that its properties can be utilized for further analysis. I take my observed relative lack of discourse about R within data science circles to mean that from a data science perspective, R² doesn’t mean more than being a performance metric like MSE or MAE.</p><p>Personally, I think we are good with using R², as long as we understand it enough to know what <em>not</em> to do with it.</p><h4>Wrapping up</h4><p>To summarize, we should expect R² to be bounded between zero and one only if a linear regression model is fit, and it is evaluated on the same data it is fitted on. Else, the definition of R² can lead to negative values.</p><p>[1] Being specific with my choice of words here :)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=17894ca26321" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/explaining-negative-r-squared-17894ca26321">Explaining negative R-squared</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[SettingWithCopyWarning in pandas]]></title>
            <link>https://medium.com/data-science/settingwithcopywarning-in-pandas-782e4aa54ff7?source=rss-7c0fe8aac76e------2</link>
            <guid isPermaLink="false">https://medium.com/p/782e4aa54ff7</guid>
            <category><![CDATA[pandas]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Tan Nian Wei]]></dc:creator>
            <pubDate>Wed, 05 Jan 2022 05:06:39 GMT</pubDate>
            <atom:updated>2022-01-06T17:31:58.547Z</atom:updated>
            <content:encoded><![CDATA[<h4>What it is, why it crops up, and how to get rid of it</h4><p>(tl;dr: jump straight to <a href="#0e7a">Getting rid of SettingWithCopyWarnings</a> if you’re here for answers)</p><p>If you are a pandas user, chances are you’ve seen the SettingWithCopyWarning crop up when you’re assigning values to a pd.DataFrame or pd.Series.</p><pre>In [1]: import pandas as pd<br>     …:<br>     …: df = pd.DataFrame({<br>     …:     “A”: [1, 2, 3, 4, 5],<br>     …:     “B”: [6, 7, 8, 9, 10],<br>     …:     }, index=range(5)<br>     …: )<br>     …: dfa = df.loc[3:5]<br>     …: dfa[“C”] = dfa[“B”] * 50</pre><pre>&lt;ipython-input-2–63497d1da3d9&gt;:9: SettingWithCopyWarning:<br>A value is trying to be set on a copy of a slice from a DataFrame.<br>Try using .loc[row_indexer,col_indexer] = value instead</pre><pre>See the caveats in the documentation: <a href="https://pandas.pydata.org/pandas-docs/">https://pandas.pydata.org/pandas-docs/</a><br>stable/user_guide/indexing.html#returning-a-view-versus-a-copy<br> dfa[“C”] = dfa[“B”] * 50</pre><p>pandas drops a warning here because the value assignment might or might not have worked as expected.</p><p>To be clear, the value assignment did occur all right; the emphasis here is on “expected”.</p><pre>In [2]: dfa<br>Out[2]:<br>  A B C<br>3 4 9 450<br>4 5 10 500</pre><pre>In [3]: df<br>Out[3]:<br>  A B<br>0 1 6<br>1 2 7<br>2 3 8<br>3 4 9<br>4 5 10</pre><p>Did you expect the contents of dfto be affected by the value assignment in dfa? pandas has internally consistent (albeit obtuse) rules on whether that happens or not. It’s just that the ambiguity in user expectations present in this situation warrants a warning, so that end users like you and me know where to look when our code misbehaves.</p><h3>Chained assignment with views and copies</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Ha5XUucUzmJZPKSZ" /><figcaption>Photo by <a href="https://unsplash.com/@kar111?utm_source=medium&amp;utm_medium=referral">Karine Avetisyan</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>The act of selecting rows or columns to access from a dataframe or series is called <em>indexing</em>. The flexibility of pandas allows for chained indexing, where you can repeatedly index the outcome of a previous indexing operation.</p><pre># Select the 2nd to 4th row of data where col A &gt; 3 and col B != 7<br>df[df[“A”] &gt; 3 &amp; df[“B”] != 7].iloc[3:5]</pre><p>pandas will then return either a view or a copy of the dataframe. A view (shallow copy) references data from the original dataframe, while a copy (deep copy) is a separate instance of the same data.</p><p>It is difficult to predict which will be returned by the indexing operation, as it depends on the memory layout of the underlying array. How exactly the indexing is chained can lead to different __getitem__ and __setitem__ calls being issued under the hood. Reproducing an example below:</p><pre># Example borrowed from [1]<br>dfmi.loc[:, (‘one’, ‘second’)] = value<br># becomes<br>dfmi.loc.__setitem__((slice(None), (‘one’, ‘second’)), value)</pre><pre>dfmi[‘one’][‘second’] = value<br># becomes<br>dfmi.__getitem__(‘one’).__setitem__(‘second’, value)</pre><p>Chain indexing inherently is not a problem, but assigning values using chained indexing, i.e. chained assignment, can be. Depending on the situation, chained assignment will either modify the original dataframe directly, or return a modified copy of the original dataframe. This can lead to insidious bugs when it is not obvious that chained indexing has occured.</p><p>Chained indexing can take places across a few lines of code:</p><pre># The following doesn’t look like chain indexing, does it?<br>dfa = df.loc[row1:row2, col1:col2]<br>…<br>…<br>dfa[row2] = dfa[row1].apply(fn)</pre><p>If pandas did not raise a warning in this scenario, it would not be obvious that df is not modified by the second value assignment. This is why the SettingWithCopyWarning exists.</p><p>pandas docs [¹] go into this with more detail. The warning message helpfully links to it, which is great because if you search pandas settingwithcopywarning on Google, the docs page is easy to miss! At time of writing, it is the 7th result on the first page of Google, and is crowded out by blogposts and StackOverflow questions.</p><h3>Peeking under the hood using the internal API</h3><p>Chained indexing is a godsend of convenience for selecting the right data, but chained assignment is a minefield for assigning the correct values. The TowardsDataScience article in [²] has a nice example where inverting the order of chained indexing alone is the difference between whether an assignment to the original dataframe occurs or not:</p><pre># Example borrowed from [2]</pre><pre># This updates `df`<br>df[“product_group”][df[“product_group”]==”PG4&quot;] = “PG14”</pre><pre># This doesn’t!<br>df[df[“product_group”]==”PG4&quot;][“product_group”] = “PG14”</pre><pre># pandas raises warnings for both<br># the user needs to confirm the intended outcome</pre><p>From <a href="https://stackoverflow.com/questions/26879073/checking-whether-data-frame-is-copy-or-view-in-pandas">this StackOverflow post</a>, pd.DataFrame and pd.Series objects have _is_view and _is_copy attributes as part of their internal API. _is_view returns True if the object is a view, and False if the object is not. _is_copy stores either a <a href="https://docs.python.org/3/library/weakref.html">weak reference</a> to the dataframe it is copied from, or None if it is not associated to an existing dataframe.</p><p>Printing these internal attributes while poking around with chained assignment does reveal some interesting tidbits of info. On one hand, pandas uses _is_copy to decide if a SettingWithCopyWarning needs to be raised. On the other hand, modifying a dataframe with _is_view = True means that it will affect the original underlying dataframe.</p><p>Before we begin, a disclaimer: internal APIs are not meant to be accessed by the end user and are subject to change, use them at your own risk.</p><pre>In [4]: pd.__version__<br>Out[4]: ‘1.3.3’</pre><pre># Setting up convenience functions<br>In [5]: def make_clean_df():<br>     …:     df = pd.DataFrame({<br>     …:         “A”: [1, 2, 3, 4, 5],<br>     …:         “B”: [6, 7, 8, 9, 10],<br>     …:         “C”: [11, 12, 13, 14, 15],<br>     …:         }, index=range(5)<br>     …:     )<br>     …:     return df</pre><pre>In [6]: def show_attrs(obj):<br>     …:     print(f”view: {obj._is_view}, copy: {obj._is_copy}”)</pre><p>We’ll start by showing the _is_view and _is_copy attributes of a few common indexing methods.</p><pre>In [7]: df = make_clean_df()</pre><pre>In [8]: show_attrs(df.loc[3:5])<br>     …: show_attrs(df.iloc[3:5])<br>     …: show_attrs(df.loc[3:5, [“A”, “B”]])<br>     …: show_attrs(df.iloc[3:5, [0, 1]])<br>     …: show_attrs(df[“A”])<br>     …: show_attrs(df.loc[:, “A”])</pre><pre>view: True, copy: &lt;weakref at 0x7f4d648b2590; to ‘DataFrame’ at 0x7f4d648b54c0&gt;<br>view: True, copy: &lt;weakref at 0x7f4d648b2590; to ‘DataFrame’ at 0x7f4d648b54c0&gt;<br>view: False, copy: None<br>view: False, copy: &lt;weakref at 0x7f4d648be770; dead&gt;<br>view: True, copy: None<br>view: True, copy: None</pre><p>Let’s break this down:</p><ul><li>Both df.loc[3:5] and df.iloc[3:5] returned views and have references to the original dataframe.</li><li>For df.loc[3:5, [“A”, “B”]] and df.iloc[3:5, [0, 1]] , when the columns are additionally specified on top of the rows, copies of df are returned instead. Using .loc indexing has no references to the OG dataframe, while using iloc indexing results in a reference to a temporary dataframe that has been garbage collected, which is as good as None itself. We’ll see if this carries any significance.</li><li>Referring to a column directly using either df[“A”] or df.loc[:, “A”] returns a view, with no reference to the original dataframe. It might have to do with the fact that each dataframe column is actually stored as a pd.Series.</li></ul><p>What happens if we manually create copies of these indexed dataframes / series?</p><pre>In [9]: show_attrs(df.loc[3:5].copy())<br>     …: show_attrs(df.iloc[3:5].copy())<br>     …: show_attrs(df.loc[3:5, [“A”, “B”]].copy())<br>     …: show_attrs(df.iloc[3:5, [0, 1]].copy())<br>     …: show_attrs(df[“A”].copy())<br>     …: show_attrs(df.loc[:, “A”].copy())</pre><pre>view: False, copy: None<br>view: False, copy: None<br>view: False, copy: None<br>view: False, copy: None<br>view: False, copy: None<br>view: False, copy: None</pre><p>Explicitly calling .copy returns copies of data that have no reference to the original dataframe / series. Assigning data on these copies will not affect the original dataframe, and thus will not trigger SettingwithCopyWarnings. Given that df.loc[3:5, [“A”, “B”]] and df.iloc[3:5, [0, 1]] above have similar attributes, we can expect that their behaviour under chained assignment should be similar to explicitly created copies.</p><p>Next, we’ll try a few chained assignment scenarios.</p><h4>Scenario 1: Specific rows indexed using loc</h4><p>The following three chained assignments raise SettingWithCopyWarnings:</p><pre>In [10]: df = make_clean_df()<br>      …: dfa = df.loc[3:5]<br>      …: show_attrs(dfa)</pre><pre>view: True, copy: &lt;weakref at 0x7fba308565e0; to ‘DataFrame’ at 0x7fba3084eac0&gt;</pre><pre># (1a)<br>In [11]: dfa[dfa % 2 == 0] = 100</pre><pre>/tmp/ipykernel_34555/3321004726.py:1: SettingWithCopyWarning: <br>A value is trying to be set on a copy of a slice from a DataFrame.<br>Try using .loc[row_indexer,col_indexer] = value instead</pre><pre>See the caveats in the documentation: <a href="https://pandas.pydata.org/pandas-docs/">https://pandas.pydata.org/pandas-docs/</a><br>stable/user_guide/indexing.html#returning-a-view-versus-a-copy<br> dfa[dfa % 2 == 0] = 100<br>/home/tnwei/miniconda3/envs/ml/lib/python3.9/site-packages/pandas/core/<br>frame.py:3718: SettingWithCopyWarning: <br>A value is trying to be set on a copy of a slice from a DataFrame</pre><pre>See the caveats in the documentation: <a href="https://pandas.pydata.org/pandas-docs/">https://pandas.pydata.org/pandas-docs/</a><br>stable/user_guide/indexing.html#returning-a-view-versus-a-copy<br> self._where(-key, value, inplace=True)</pre><pre># (1b)<br>In [12]: dfa[“D”] = dfa[“B”] * 10</pre><pre>/tmp/ipykernel_34555/447367411.py:1: SettingWithCopyWarning: <br>A value is trying to be set on a copy of a slice from a DataFrame.<br>Try using .loc[row_indexer,col_indexer] = value instead</pre><pre>See the caveats in the documentation: <a href="https://pandas.pydata.org/pandas-docs/">https://pandas.pydata.org/pandas-docs/</a><br>stable/user_guide/indexing.html#returning-a-view-versus-a-copy<br> dfa[“D”] = dfa[“B”] * 10 # 1b</pre><pre># (1c)<br>In [13]: dfa[“A”][3] = 10</pre><pre>/tmp/ipykernel_34555/1338713145.py:1: SettingWithCopyWarning: <br>A value is trying to be set on a copy of a slice from a DataFrame</pre><pre>See the caveats in the documentation: <a href="https://pandas.pydata.org/pandas-docs/">https://pandas.pydata.org/pandas-docs/</a><br>stable/user_guide/indexing.html#returning-a-view-versus-a-copy<br> dfa[“A”][3] = 10</pre><p>All of the value assignments took effect on dfa itself, but only (1a) and (1c) affected the original dataframe. (1b) did not.</p><pre>In [14]: print(dfa)<br> <br>  A  B  C   D<br>3 10 9  100 90<br>4 5  100 15 1000</pre><pre>In [15]: print(df)</pre><pre>  A  B  C<br>0 1  6  11<br>1 2  7  12<br>2 3  8  13<br>3 10 9  100<br>4 5  100 15</pre><p>In addition, dfa is no longer a view, but a copy of the dataframe!</p><pre>In [16]: show_attrs(dfa) # view changed to False</pre><pre>view: False, copy: &lt;weakref at 0x7fba308565e0; to ‘DataFrame’ at 0x7fba3084eac0&gt;</pre><p>What this tells us is that pandas will convert a view to a copy when necessary. This further shows why figuring out chained assignment is inherently tricky, and is difficult to cater for automatically at the library level.</p><h4>Scenario 2: Specific rows indexed using iloc</h4><p>This is the same as scenario 1, but using iloc instead.</p><pre>In [17]: df = make_clean_df()<br>      …: dfb = df.iloc[3:5]<br>      …: show_attrs(dfb)</pre><pre>view: True, copy: &lt;weakref at 0x7fba30862040; to ‘DataFrame’ at 0x7fba30868c10&gt;</pre><pre># (1a)<br>In [18]: dfb[dfb % 2 == 0] = 100</pre><pre>/tmp/ipykernel_34555/734837801.py:1: SettingWithCopyWarning: <br>A value is trying to be set on a copy of a slice from a DataFrame.<br>Try using .loc[row_indexer,col_indexer] = value instead</pre><pre>See the caveats in the documentation: <a href="https://pandas.pydata.org/pandas-docs/">https://pandas.pydata.org/pandas-docs/</a><br>stable/user_guide/indexing.html#returning-a-view-versus-a-copy<br> dfb[dfb % 2 == 0] = 100<br>/home/tnwei/miniconda3/envs/ml/lib/python3.9/site-packages/pandas/core/<br>frame.py:3718: SettingWithCopyWarning: <br>A value is trying to be set on a copy of a slice from a DataFrame</pre><pre>See the caveats in the documentation: <a href="https://pandas.pydata.org/pandas-docs/">https://pandas.pydata.org/pandas-docs/</a><br>stable/user_guide/indexing.html#returning-a-view-versus-a-copy<br> self._where(-key, value, inplace=True)</pre><pre><br># (1b)<br>In [19]: dfb[“D”] = dfb[“B”] * 10</pre><pre>/tmp/ipykernel_34555/4288697762.py:1: SettingWithCopyWarning: <br>A value is trying to be set on a copy of a slice from a DataFrame.<br>Try using .loc[row_indexer,col_indexer] = value instead</pre><pre>See the caveats in the documentation: <a href="https://pandas.pydata.org/pandas-docs/">https://pandas.pydata.org/pandas-docs/</a><br>stable/user_guide/indexing.html#returning-a-view-versus-a-copy<br> dfb[“D”] = dfb[“B”] * 10</pre><pre># (1c)<br>In [20]: dfb[“A”][3] = 10</pre><pre>/tmp/ipykernel_34555/2062795903.py:1: SettingWithCopyWarning: <br>A value is trying to be set on a copy of a slice from a DataFrame</pre><pre>See the caveats in the documentation: <a href="https://pandas.pydata.org/pandas-docs/">https://pandas.pydata.org/pandas-docs/</a><br>stable/user_guide/indexing.html#returning-a-view-versus-a-copy<br> dfb[“A”][3] = 10</pre><p>The observed outcome is the same as Scenario 1.</p><pre>In [21]: print(dfb)<br>  A  B  C   D<br>3 10 9  100 90<br>4 5  100 15 1000</pre><pre>In [22]: print(df)</pre><pre>  A  B   C<br>0 1  6   11<br>1 2  7   12<br>2 3  8   13<br>3 10 9   100<br>4 5  100 15</pre><pre>In [23]: show_attrs(dfb)</pre><pre>view: False, copy: &lt;weakref at 0x7fba30862040; to ‘DataFrame’ at 0x7fba30868c10&gt;</pre><h4>Scenario 3: Specific rows and columns indexed using loc</h4><p>Same as Scenario 1, but the columns are specified as well.</p><pre>In [24]: df = make_clean_df()<br>      …: dfc = df.loc[3:5, [“A”, “B”]]<br>      …: show_attrs(dfc)</pre><pre>view: False, copy: None</pre><pre>In [25]: dfc[dfc % 2 == 0] = 100 # No warnings raised<br>      …: dfc[“D”] = dfc[“B”] * 10<br>      …: dfc[“A”][3] = 10</pre><p>No warnings raised. All changes took effect on `dfc` without impacting `df`.</p><pre>In [26]: print(dfc)<br> <br>  A  B  D<br>3 10 9  90<br>4 5  100 1000</pre><pre>In [27]: print(df)</pre><pre>  A  B   C<br>0 1  6   11<br>1 2  7   12<br>2 3  8   13<br>3 10 9   100<br>4 5  100 15</pre><p>The chained assignment outcome is different, while the data indexed is the same as in Scenario 1. My guess is that a more complete description of the indexing operation prompted pandas to directly return a copy upfront, instead of a view that is linked to the original dataframe.</p><h4>Scenario 4: Specific rows and columns indexed using iloc</h4><p>This is similar to Scenario 3, but using iloc instead. Given the past few scenarios, it is no surprise that this scenario had the same outcome as Scenario 3.</p><pre>In [28]: df = make_clean_df()<br>      …: dfd = df.iloc[3:5, [0, 1]]<br>      …: show_attrs(dfd)</pre><pre>view: False, copy: &lt;weakref at 0x7fba306f29f0; dead&gt;</pre><pre>In [29]: dfd[dfd % 2 == 0] = 100 # No warnings raised<br>      …: dfd[“D”] = dfd[“B”] * 10<br>      …: dfd[“A”][3] = 10</pre><pre>In [30]: print(dfd)</pre><pre>  A  B   D<br>3 10 9   90<br>4 5  100 1000</pre><pre>In [31]: print(df)</pre><pre>  A B  C<br>0 1 6  11<br>1 2 7  12<br>2 3 8  13<br>3 4 9  14<br>4 5 10 15</pre><p>In addition, `dfd` discarded the reference to the garbage-collected dataframe at the end of this code.</p><pre>In [32]: show_attrs(dfd)</pre><pre>view: False, copy: None</pre><h4>Scenario 5: Directly referring to a column of a dataframe</h4><p>This scenario tests chained assignment on series.</p><pre>In [33]: df = make_clean_df()<br>      …: dfe = df[“A”]<br>      …: show_attrs(dfe)</pre><pre>view: True, copy: None</pre><pre>In [34]: dfe[1] = 99999 # No warnings raised<br>      …: dfe.loc[2:4] = 88888</pre><p>dfe remained a view of df[“A”]. All changes effected on dfe is reflected in df[“A”], which is still part of df. It appears that there’s not much to worry about for chained assignment on individual series.</p><pre>In [35]: print(dfe)</pre><pre>0 1<br>1 99999<br>2 88888<br>3 88888<br>4 88888<br>Name: A, dtype: int64</pre><pre>In [36]: print(df)</pre><pre>  A     B C<br>0 1     6 11<br>1 99999 7 12<br>2 88888 8 13<br>3 88888 9 14<br>4 88888 10 15</pre><pre>In [37]: show_attrs(dfe)</pre><pre>view: True, copy: None</pre><h3>Getting rid of SettingWithCopyWarnings</h3><p>SettingWithCopyWarnings pop up when pandas isn’t sure if you want value assignment to affect the original dataframe or not. Therefore, getting rid of these warnings entails avoiding ambiguity in value assignment. As seen from the code samples above, getting pandas to return copies with no reference to the original dataframe is a clean way to ensure that values will not be written to the original dataframe unintended.</p><p>I’ve found this to be a unifying thread across the solutions I came across when researching this topic. Summarizing them below:</p><h4>Disabling the warnings</h4><p>If you know what you’re doing and your code is behaving as intended, you can choose to suppress the warnings by disabling them [³]:</p><pre># Example borrowed from [³]</pre><pre># Disables SettingWithCopyWarning globally<br>pd.set_option(‘mode.chained_assignment’, None)</pre><pre># Resets the warning option to default<br>pd.reset_option(‘mode.chained_assignment’)</pre><pre># Disables SettingWithCopyWarning locally within a context manager<br>with pd.option_context(‘mode.chained_assignment’, None):<br>    # YOUR CODE HERE</pre><p>Alternatively, you can suppress the warnings by setting the dataframe _is_copy attribute to None [³].</p><pre># Example modified from [3]<br>In [38]: df = pd.DataFrame({<br>      …:     “A”: [1, 2, 3, 4, 5],<br>      …:     “B”: [6, 7, 8, 9, 10],<br>      …:     “C”: [11, 12, 13, 14, 15],<br>      …: }, index=range(5))<br>      …: <br>      …: dfa = df.loc[3:5]<br>      …: print(dfa._is_copy)</pre><pre>&lt;weakref at 0x7f4d64792810; to ‘DataFrame’ at 0x7f4d64784460&gt;</pre><pre>In [39]: dfa._is_copy = None<br>      …: dfa[“D”] = dfa[“B”] * 10 # No warning raised</pre><p>Remember that making the warnings go away doesn’t resolve wonky chained assignment issues. Chained assignment is a minefield where you might or might not step on a landmine. Disabling the warnings is like removing the minefield warning signs. Food for thought.</p><h4>Making the warnings not show up in the first place</h4><p>When you run into a SettingWithCopy warning, take a moment to trace the chained assignment and decide if you want to modify the original dataframe directly, or have values assigned to a copy of the dataframe.</p><h4>Working on the original dataframe</h4><p>Use .loc indexing to directly assign values to the dataframe.</p><pre># Modified from examples in [2]<br>In [40]: df = pd.DataFrame({<br> …: “A”: [1, 2, 3, 4, 5],<br> …: “B”: [6, 7, 8, 9, 10],<br> …: “C”: [11, 12, 13, 14, 15],<br> …: }, index=range(5))<br> …:<br> …: df.loc[df[“A”] % 2 != 0, “B”] = df.loc[df[“A”] % 2 != 0, “B”] + 0.5<br> …: print(df)</pre><pre>A B C<br>0 1 6.5 11<br>1 2 7.0 12<br>2 3 8.5 13<br>3 4 9.0 14<br>4 5 10.5 15</pre><p>pandas docs recommend this method for two reasons:</p><ul><li>using .loc is guaranteed to refer to the underlying dataframe it is called on. .iloc does not have this property.</li><li>.loc indexing replaces what could be chain indexing into a single indexing step. If you refer to the example above under <a href="#5424">Chained assignment with views and copies</a>, .loc indexing resolves chained indexing into a single __setitem__ call.</li></ul><p>If you are selecting data using conditionals, you can consider returning a mask instead of a copy of the original dataframe. A mask is a boolean series or dataframe which can conveniently be used in .loc indexing, as the example below:</p><pre># Modified from examples in [5]<br>In [41]: df = pd.DataFrame({<br>      …:     “A”: [1, 2, 3, 4, 5],<br>      …:     “B”: [6, 7, 8, 9, 10],<br>      …:     “C”: [11, 12, 13, 14, 15],<br>      …: }, index=range(5))</pre><pre>In [42]: dfa = (df[“A”] &lt;= 3) &amp; (df[“C”] == 12)</pre><pre>In [43]: df.loc[dfa, “B”] = 99 # dfa can be fed into the loc index!</pre><pre>In [44]: print(df) # changes took place in the original dataframe</pre><pre>  A B  C<br>0 1 6  11<br>1 2 99 12<br>2 3 8  13<br>3 4 9  14<br>4 5 10 15</pre><p>Working on the original dataframe directly can be tricky if existing indexing logic is complex. In that case, you can always use one of the methods from the next section to return a copy, then assign it back to the original dataframe [⁴].</p><h4>Assigning values to an explicit copy of the dataframe</h4><p>Use assign, where and replace:</p><pre>In [45]: df = pd.DataFrame({<br>      …:     “A”: [1, 2, 3, 4, 5],<br>      …:     “B”: [6, 7, 8, 9, 10],<br>      …:     “C”: [11, 12, 13, 14, 15],<br>      …: }, index=range(5))</pre><pre># 1. Use the `assign` method to add columns<br>In [46]: df = df.assign(D=df[“C”] * 10)<br>      …: df = df.assign(**{“D”: df[“C”] * 10}) # allows passing variables as names</pre><pre># 2. Use the `where` method to select values using conditionals and replace them<br># Modified from examples in [2]<br>In [47]: df[“B”] = df[“B”].where(<br>      …:     df[“A”] &lt; 2, df[“B”] * 10<br>      …: )</pre><pre># 3. Use the `replace` method to select and replace values in the dataframe<br># Modified from examples in [2]<br>In [48]: df = df.replace({“A” : 1}, 100)</pre><pre>In [49]: print(df)<br>  A   B  C  D<br>0 100 6  11 110<br>1 2  70  12 120<br>2 3  80  13 130<br>3 4  90  14 140<br>4 5  100 15 150</pre><p>Break down chained assignment steps into single assignments [⁵]:</p><pre># Examples borrowed from [4]<br># Not these<br>df[“z”][mask] = 0<br>df.loc[mask][“z”] = 0</pre><pre># But this<br>df.loc[mask, “z”] = 0</pre><p>A less elegant but foolproof method is to manually create a copy of the original dataframe and work on it instead [²]. As long as you don’t introduce additional chained indexing, you will not see the SettingWithCopyWarning.</p><pre>In [50]: df = pd.DataFrame({<br>      …:     “A”: [1, 2, 3, 4, 5],<br>      …:     “B”: [6, 7, 8, 9, 10],<br>      …:     “C”: [11, 12, 13, 14, 15],<br>      …: }, index=range(5))</pre><pre>In [51]: dfa = df.loc[3:5].copy() # Added .copy() here<br>      …: dfa.loc[3, “A”] = 10 # causes this line to raise no warning</pre><h3>Redoing some of the examples above without triggering SettingWithCopy warnings</h3><p>Replace a chained assignment with where:</p><pre># (i)<br>df = make_clean_df()<br>dfa = df.loc[3:5]</pre><pre># Original that raises warning<br># dfa[dfa % 2 == 0] = 100</pre><pre>dfa = dfa.where(dfa % 2 != 0, 100) # df is not affected</pre><p>Replace creating a new column on the indexed dataframe with assign:</p><pre># (ii) <br>df = make_clean_df()</pre><pre># Original that raises warning<br># dfa[“D”] = dfa[“B”] * 10</pre><pre>dfa = dfa.assign(D=dfa[“B”]*10) # df is not affected</pre><p>Creating a copy of the dataframe, before assigning values using .loc indexing:</p><pre># (iii)<br>df = make_clean_df()</pre><pre># Original that raises warnings<br># dfa = df.loc[3:5]<br># dfa[“A”][3] = 10</pre><pre># Create a copy then do loc indexing<br>dfa = df.loc[3:5].copy()<br>dfa.loc[3, “A”] = 10</pre><p>Note that directly assigning values to dfa using .loc indexing will still raise a warning, as it is ambiguous if the assignment to dfa should also affect df.</p><h3>Truly rooting out SettingWithCopyWarnings</h3><p>Personally, I am a fan of promoting SettingWithCopyWarnings to SettingWithCopyExceptions for important scripts, using the following code:</p><pre>pd.set_option(‘mode.chained_assignment’, “raise”)</pre><p>Doing this forces chained assignment to be dealt with, rather than allowing warnings to accumulate.</p><p>In my experience, cleaning up notebooks with stderr clogged by SettingWithCopyWarnings is its special kind of zen. I wholeheartedly recommend it.</p><p>Original blogpost: <a href="https://tnwei.github.io/posts/setting-with-copy-warning-pandas/">SettingWithCopyWarnings in pandas</a></p><p>[¹]: Official `pandas` docs on chained assignment.<br>[<a href="https://pandas.pydata.org/docs/user_guide/indexing.html#returning-a-view-versus-a-copy](https://pandas.pydata.org/docs/user_guide/indexing.html#returning-a-view-versus-a-copy)">https://pandas.pydata.org/docs/user_guide/indexing.html#returning-a-view-versus-a-copy](https://pandas.pydata.org/docs/user_guide/indexing.html#returning-a-view-versus-a-copy)</a></p><p>[²]: TowardsDataScience article that briefly touches on a few ways to deal with the SettingWithCopy warnings.<br>[<a href="https://scribe.rip/@towardsdatascience.com/3-solutions-for-the-setting-with-copy-warning-of-python-pandas-dfe15d62de08](https://scribe.rip/@towardsdatascience.com/3-solutions-for-the-setting-with-copy-warning-of-python-pandas-dfe15d62de08)">https://scribe.rip/@towardsdatascience.com/3-solutions-for-the-setting-with-copy-warning-of-python-pandas-dfe15d62de08](https://scribe.rip/@towardsdatascience.com/3-solutions-for-the-setting-with-copy-warning-of-python-pandas-dfe15d62de08)</a></p><p>[³]: In-depth article on this topic by DataQuest. Notably, there is a section dedicated to the history of dealing with chained assignment in `pandas`.<br>[<a href="https://www.dataquest.io/blog/settingwithcopywarning/](https://www.dataquest.io/blog/settingwithcopywarning/)">https://www.dataquest.io/blog/settingwithcopywarning/](https://www.dataquest.io/blog/settingwithcopywarning/)</a></p><p>[⁴]: StackOverflow post that contains more chained assignment examples. [<a href="https://stackoverflow.com/questions/48173980/pandas-knowing-when-an-operation-affects-the-original-dataframe](https://stackoverflow.com/questions/48173980/pandas-knowing-when-an-operation-affects-the-original-dataframe)">https://stackoverflow.com/questions/48173980/pandas-knowing-when-an-operation-affects-the-original-dataframe](https://stackoverflow.com/questions/48173980/pandas-knowing-when-an-operation-affects-the-original-dataframe)</a></p><p>[⁵]: RealPython article covering this topic. For me, RealPython is a trusted goto reference second to official library docs. This article further goes into depth on the underlying view vs copy mechanisms in pandas, and in numpy, which pandas depends on.<br>[<a href="https://realpython.com/pandas-settingwithcopywarning/](https://realpython.com/pandas-settingwithcopywarning/)">https://realpython.com/pandas-settingwithcopywarning/](https://realpython.com/pandas-settingwithcopywarning/)</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=782e4aa54ff7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/settingwithcopywarning-in-pandas-782e4aa54ff7">SettingWithCopyWarning in pandas</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>