Search
SE Radio Guests Jeroen Janssens and Thijs Nieuwdorp

SE Radio 727: Jeroen Janssens and Thijs Nieuwdorp on Using Polars

Jeroen Janssens, a senior developer relations engineer at Posit, and Thijs Nieuwdorp, a developer relations engineer at Polars, speak with host Gregory M. Kapfhammer about Polars, a Python package for transforming, analyzing, and visualizing data. After discussing the key features, they explore the implementation and use of the expressions data type provided by Polars. Along with comparing Polars to other data-manipulation packages like Pandas, they also share best practices for performing data analysis in Python with Polars. Jeroen, Thijs, and Gregory also discuss topics such as how to interface Polars with a SQL database.

Brought to you by IEEE Computer Society and IEEE Software magazine.

banner ad that says turn your knowledge into recognition - Software Professional Certification



Show Notes

Related Episodes

Other References


Transcript

Transcript brought to you by IEEE Software magazine.
This transcript was automatically generated. To suggest improvements in the text, please contact [email protected] and include the episode number and URL.

Gregory Kapfhammer 00:00:18 Welcome to Software Engineering Radio. I’m your host, Gregory Kapfhammer. I’m excited today to have an episode about Polars. Today’s guests are Jeroen Janssens, the Head of Developer Relations at Posit, and Thijs Nieuwdorp, a Developer Relations Engineer at Polars. Hi to both of you and welcome to Software Engineering Radio.

Jeroen Janssens 00:00:42 Thank you for having us. Glad to be here. Thank you Gregory.

Gregory Kapfhammer 00:00:44 So thanks for you being willing to talk to us today about Polars. We’re going to talk about the Polars ecosystem. We’re going to learn all about how data engineers can use Polars and we’re going to be building our conversation today on the book that the two of you wrote called Python Polars, the Definitive Guide. So, with those introductory points in mind, are you both ready to dive in and get started?

Jeroen Janssens 00:01:09 Absolutely.

Gregory Kapfhammer 00:01:09 Alright, cool. So, what I wanted to do at the beginning is to start with a short quotation from the Polars documentation and then I’m going to ask you Jeroen to unpack this quotation and tell us a little bit more about Polars. So, what I read was that it’s a blazingly fast data frame library for manipulating structured data. I read that the core is written in Rust and it’s available for Python, R and Node.js. Can you unpack those two sentences in a little bit more detail and tell us about Polars?

Jeroen Janssens 00:01:41 Right. So, the best way to explain how Polars came to be is through a personal story from Ritchie Vink, who is the creator of Polars. And that’s actually the whole re of this library. Now, about five years ago, Ritchie was trying to join two tables for some client work that he had to do, and he just was not satisfied with how slow pandas were for that task. So, Ritchie being Ritchie, he tried to implement this himself using Rust. He wanted to learn Rust, this new fast language. And at first, it was way slower than pandas, but again, Ritchie being Ritchie, he just kept at it and kept improving it. And now fast forward five years, we have this incredibly fast library with an expressive API for working with data. And now the main reason why data scientists and engineers are giving Polars a spin is still the need for raw speed.

Jeroen Janssens 00:02:42 Now within the community, we often say that you come for the speed, but you stay for the API when it comes to Polars. And I’m sure that we get into why that API is so special at a later stage. But I think it’s also now a good time to explain how not only Polars came into view, but also the book, right? You mentioned our book and the simple reason is that the three of us, we were at some point all working at the same company, Xomnia. So that’s also the birthplace of Polars itself. That’s also where Thijs and I got a chance to implement Polars at a client right, into production code. And one thing led to another and now Polars have evolved even more. We are well past the 1.0 mark. There’s now a book and Polars keep adding features and keeps getting faster and faster.

Gregory Kapfhammer 00:03:34 Thanks for that response. That was really helpful. I wanted to double click on one thing that you said, and I remember reading in the documentation, so Jeroen, we said that it’s a data frame library for manipulating structured data. Can you tell our listeners what is a data frame library and why do we need it for structured data?

Jeroen Janssens 00:03:53 Right, so a data frame is kind of like a table that you would have in your database, just a single table where you have rows and columns, right? Those columns, all the values in the same column have to be of the same type. But across your data frame you can have a variety of columns that all have different data types. And that is really the structure of your data, this rectangular structure. Another way to think of it is a sheet in the spreadsheet.

Gregory Kapfhammer 00:04:22 Okay, that makes a lot of sense. So, it’s a sheet in a spreadsheet or inside of a Google sheet. And Thijs, I wanted to turn to you now because I extracted three phrases that came from the Polars documentation and I think they’re all key phrases. So, the first is something that I mentioned a moment ago, but it says that it’s fast and designed close to the machine without external dependencies. Can you develop that quotation in greater detail?

Thijs Nieuwdorp 00:04:49 Yeah, so I think we all know as programmers what fast is supposed to mean, right? So high performance and the close to the machine part is that photos are built from the ground up, like all the way from IO through the transformations and back to IO again. So, make sure that the performance critical path is in our own hands, so we don’t depend on the other libraries that do the writing for us. And in this way, we can squeeze out the maximum performance for our entire pipeline.

Gregory Kapfhammer 00:05:15 Okay, that makes sense. Now the next phrase that I thought was important is related to intuitive API. Can you tell us why is the API for Polars considered intuitive for a data engineer?

Thijs Nieuwdorp 00:05:28 Yeah, so an intuitive API according to us is the one where a developer can correctly guess how to use it before reading the documentation. So, if you know the terminology in the field, there’s a lot of like you have transformations like Joins, group buys, and those are all in the API. And on top of that, there’s keywords, arguments, other transformations that you can do that do what you expect them to. So, there’s no surprises and a lot of consistency in the API. And through that we also thought about default behavior. It should be correct for the common case. So, there’s no foot guns as they’re often called.

Gregory Kapfhammer 00:06:02 Okay, I know what you mean by a foot gun. And I have experienced that when I’ve been writing my own data analysis code on many occasions. Now we’re going to talk a little bit more about the API and why it’s intuitive and we’ll give examples later in the show. But the third phrase that I wanted you to unpack for our listeners is the phrase ìout of coreî. Because I understand that Polars has something called a streaming API and I’m hoping you can develop that a little bit further.

Thijs Nieuwdorp 00:06:28 Absolutely. So ìout of coreí indicates a set of algorithms that is designed to process data that are too large to fit into a computer’s main memory all at once. And what the streaming engine does is it chops the data up in parts and process it as far as it can go, generally reducing peak memory usage. And of course, depending on the circumstances like the dataset and the query, oftentimes you can process datasets that are larger than RAM. And one of the things that we’re currently still working on is a spill to disc functionality where for example, in the case when you have a sort, you require the entirety of the data before you can sort it of course, because you need to know all the values that are in there to put them in the right order. This can be the case, like if you have very large data sets that even in the streaming engine doesn’t fit into RAM anymore. Now you would get out of memory errors, but over time we would spill some of that data at the disk like intermediate results. So, we can still process that like at the cost of some runtime performance, it’ll still finish.

Gregory Kapfhammer 00:07:25 Okay, I’m getting it. Now the other thing that I noticed about Polars is that it’s what’s called a columnar-oriented data frame. Could one of you explain the idea of it being column-oriented or columnar in nature?

Thijs Nieuwdorp 00:07:38 Well, since I wrote the chapter on this, I can definitely explain that. So, one of the hallmarks of Polars is that its built-on Apache Arrow. And Apache Arrow defines itself as a multi-language toolbox for building high-performance applications that process and transport large data sets. So, one of the main aspects of Apache Arrow is its columnar storage. And to do explain why they picked this and why it makes sense in an analytical query context, I’ve got a metaphor for that. So, you can imagine a filing cabinet and in it you store all your sale data and if you have a row-based format, like a relatively traditional format, each of these drawers of the filing cabinet contains all the data that you need for one single sale. So, you have like the person something was sold to, what items were sold, the price of the sale when the sale happened, like all that data of a sale is in one drawer.

Thijs Nieuwdorp 00:08:28 The problem about that is if you always want to dig up all the information about a sale you make, that makes sense because like you need to have everything in a drawer, but in analytical queries it’s more often necessary that you only take parts of that data. Like you only take one column, you only take the person, or you only take the items and you don’t need the date of a sale. And the problem with the traditional storing format is that you would have to open up all of the drawers to be able to just run your query in any case, even if you only need a part of a drawer. So if you change that around, if you order your cabinet in a manner where every row contains for example an identifier and all like just all the names of people, then if you just want to get the name and the items they bought, you open up two drawers and you’re done.

Thijs Nieuwdorp 00:09:08 You don’t have to open all the cabinets. And this is something that in analytical queries is more often used and that’s why the columnar approach makes a lot of sense. And one of the added advantages of Arrow is that it’s a specification of how stuff is stored in memory. So, if Polars has implemented Apache Arrow, other libraries that also implement it can very easily interoperate. So, we see this, for example, pandas now has also an Apache Arrow backend that you can choose, and you can very easily without copying too much data, copy data from one tool to the other instead of having to serialize and deserialize it. There’s a couple of advantages and these are the main ones.

Gregory Kapfhammer 00:09:44 Okay, that was helpful. So, we know that Polars is a data frame library and you both have mentioned details that we’re going to pick up on later in the show. In a moment, I wanted to start focusing on what data frame is about and then I wanted to talk about Polar expressions, in particular. But before we move on to those two topics, are there any other key points that you think our listeners need to know before we dive into how to program with Polars?

Jeroen Janssens 00:10:11 Well, so Polars has bindings for multiple languages and the library itself is built in Rust, but it has bindings in Python, Node.js, R. Python is by far the most complete and popular one. So, if you want to use Polars effectively, then you should be familiar with Python.

Gregory Kapfhammer 00:10:32 Okay. Alright, that’s helpful. And if our listeners are interested in learning more about how you can build Python tooling and Rust, I would encourage them to check out Episode 622 where we talked all about specifically that topic and Jeroen, thanks for clarifying. You can write Polars code in Node.js or in Python as two examples, but it sounds like the best environment for doing Polars programming is in Python. And so, we’ll use that as a vehicle for a lot of our follow-on conversation. Now I wanted to go back to this concept of Polars being a data frame library. And you mentioned that it’s a little bit like a Google sheet or an Excel spreadsheet. And so, I think that means that it’s a two-dimensional data structure and I can manipulate the data or that I can analyze the data. And I know our listeners are going to be interested in various types of complex data operations like merging or aggregation. So Jeroen, can you pick up the idea of merging and aggregation and tell people how we do that in Polars?

Jeroen Janssens 00:11:33 Yeah, so to understand how Polars is different from other libraries, right? Or pandas and what kind of patterns they need to adopt, it really depends on where they’re coming from. You may have experienced a MATLAB where everything is a matrix or SQL, R or pandas. And if you’re coming from Pandas like Thijs and I did, then perhaps the biggest shift that you have to make is that you have to let go of the row index. because Polars does not use it for data manipulation. Another one is that you won’t be writing any more brackets, right? In Pandas code you have brackets all over the place, but in Polars you don’t need them. And so when you’re coming from SQL, the switch is maybe more natural because Polars also uses this declarative style where you just tell the library what you want the final result to look like. Then Polars will use its own internal optimizer to figure out the most efficient way to execute the query that you wrote.

Gregory Kapfhammer 00:12:34 Aha, that’s really helpful. Could I just jump in quickly? You mentioned the idea of Polars being declarative in nature and I think we would also say that SQLs declarative in nature, and since you’ve mentioned pandas in passing, can you tell us just briefly a little bit more about pandas and then put pandas in the context of words like declarative or is it something different?

Jeroen Janssens 00:12:57 Yeah, with pandas you have to, or at least I had to do a lot more thinking in the operations themselves, right? So, it was much more procedural. I had to tell pandas exactly in what order it should do things with Polars and especially when you use the lazy API, we’ll get to that. You don’t have to worry about that so much because Polars takes the opportunity to reorder certain operations and to optimize what it actually needs to do.

Gregory Kapfhammer 00:13:30 Okay, that’s helpful. So, when I think of declarative, I should think of something like SQL or Polars. And the idea is I declare to the engine what I want and it optimizes for me the actual procedure for getting the data that I need. When it comes to merging or aggregating the data, is that the right way to think about it?

Jeroen Janssens 00:13:52 That is the right way. An analogy that I also like to use for those who are familiar with this, R has a very popular data visualization package called GGplot2. The GG stands for Grammar of Graphics, which is also declarative. So rather than saying, I want you to group the data and then write a for loop where you loop over these groups and give every group a different color, right? In order to create a say a colored scattered plot, which would be the procedural way that you use in many other data visualization packages. Instead, what you do is, okay, I want you to create a scatterplot and I want the color to be based on this column. And then GGplot2 figures out how it should do that. And that is also then a more declarative way of creating your data visualizations.

Gregory Kapfhammer 00:14:43 Thanks, that was fantastic and you reminded me of the many hundreds of hours I have spent writing GGplot2 code. But for now, we won’t talk about that any further, although we will get to visualizations later. What I wanted to do is really hone in on the idea of an expression, because I think the word expression is one that’s intuitively clear and that we use in abstract syntax trees or broadly in programming. But I think Polars has a very specific understanding of the word expression. So, I’d like to dwell here and ask the two of you together to help our listeners to understand what expressions are and how they work in Polars.

Jeroen Janssens 00:15:23 Yes, expressions are small things, but they are vital to Polars itself. In fact, they play such a big role that when we set out to write the book, we anticipated to only dedicate a single chapter to this. Turns out we need three nearly a hundred pages were spent on explaining everything that you can do with expressions. And I think it’s safe to say that when you understand expressions, you’ve mastered over 80% of the API that Polars offers and it’s a big API. So, think of expressions as small operations that you can stack together to create a full query and chaining them, meaning adding a method to an existing object, right? Method chaining as Python programmers are very fond of as opposed to when you’re working in R which is more functional. So, when you chain them, it means that you can read your code from left to right or top to bottom like a recipe.

Jeroen Janssens 00:16:25 And that’s much easier to follow because it keeps the logic clean. Because you aren’t creating a bunch of messy temporary variables everywhere. So when you write a book, right, we spent a lot of time on this, you also start to think about these concepts when you have a lot of time on your hands, you like to come up with definitions and the definitions that you may come across, in the Polars documentation or other books, they all slightly differ. Now I do want to share the definition that we came up with in our book. Funny thing actually is that we discussed this with Ritchie, the creator of Polars, and actually we weren’t a hundred percent clear on what an expression really is, but this is how we define it: An expression is a tree of operations that describe how to construct one or more series.

Jeroen Janssens 00:17:16 Now there are a few concepts in here that we haven’t yet heard about, but when you think of it, it’s, let’s start at the very end. A series, right? A series is a sequence of values that all have the same data type. And very often in practice that’s a column in a data frame. So, you use expressions to update an existing column or to create a new one or to define how your grouping should be done or how your data should be filtered. These expressions are everywhere when you’re writing Polars code. So that’s a series. You can create one or more. So, with the same expression you can actually say, let’s say you have 10 numerical columns, then with a single expression you can do the same operation to all of those columns in one go. Very efficient and very readable as well. Now operations because they do things to your data.

Jeroen Janssens 00:18:13 But when I really started to think about expressions, okay, what are they really? I started to understand, okay, they’re actually a tree of operations. So, let’s say you want to sum two columns together. What you do is you reference to the first column, right? You do that using the col() function in the Polars namespace. So that could be pl.col(), and then x in quotes and parentheses, of course, because col is a function. Now that, in itself, references an existing column in your data frame and that’s already an expression. It doesn’t do anything by itself, right? It’s a description, it’s a recipe that tells you how to do certain things. Now maybe we want to do something with this column, let’s add another column to it. Let’s say we have two numerical columns, x and y. So, we write pl.col(“x”) + pl.col(“y”). Now these are two small expressions actually combined using the addition operator into a new expression, right? So that’s where the tree comes from. You gradually build your expressions in that way. And because they appear in so many different places, whether it’s selecting columns, whether it’s creating new ones, filtering, sorting, aggregation, because they’re everywhere, you get a lot of exposure to them, they become second nature, and writing Polars code becomes, to be honest, quite a joy.

Gregory Kapfhammer 00:19:48 Okay, that was really helpful. Now you’ve mentioned several topics that I want to pick up on in greater detail, but before I do that, Thijs could I turn to you? Could you please give our listeners a concrete example of a simple but realistic data engineering routine that they might be able to express using expressions in Polars? Is there something you could do to make this concrete for us?

Thijs Nieuwdorp 00:20:12 Absolutely. So just off the top of my head, let’s see where we can go for. So, one of the things, often times used in aggregations when you have a data frame that you have like sales data in it, for example, and you want to group by stores just to get some performance off of stores specifically. So, for every store you want to do some aggregation. Over there you can very easily grab a column with all the sales. For example, you want to compute the revenue for a store. Like it’s a relatively simple aggregation, right? What you would do is you take the data frame, you do a group_by on the stores or the store ID, and in the aggregation context you start with the pl.col() and you mark for example the price, the total price. And on that you would simply perform a .sum(), and that that’s like one of the most simple expressions that you could get.

Gregory Kapfhammer 00:21:00 Great, that’s helpful. Now, a moment ago Jeroen, you mentioned the idea of function chaining, and when you were talking it made me think about how when I’ve programmed an R, I often used a package that is called dplyr. And I recognize some of our listeners may have heard of dplyr. I know that it’s not the key focus of our episode, but are there connections between the way you would analyze data using dplyr in R and the way that you analyze data with pandas in Python? Is there anything more that you could unpack there?

Jeroen Janssens 00:21:31 Yeah, so I’ve heard from quite a few people, especially those coming from R and looking to switch to Python because perhaps their organization wants them to, is that they often say that they do like Polars better than pandas because it’s more closely to what they’re used to when working with dplyr and other packages in the Tidyverse. Now I think at first glance there is some similarity between method chaining, right? Where you say, okay, first I do this, then I do that, by adding a dot and then some method name and then another dot and a method name and you gradually build up your expression and combining functions using the pipe operator in the Tidyverse. There is, however, a big difference between the two because in R, because they’re all functions, it is actually much easier to add your own functionality and immediately use that in some pipeline or functions from different packages that work nicely with the same philosophy that all these functions across the Tidyverse have, which is the data is the first argument and the output is also data.

Jeroen Janssens 00:22:49 It’s kind of also conceptually similar to how pipe operators on the command line work. But let’s not dwell too much into that, but it seems that people who are working in Python are okay with that, with the limitation that when you are doing method chaining, you are bound to the methods that are provided to you because a method is bound to an object. So, there are ways and Polars to work around this, right? We there is the concept of a plugin so that you can extend functionality and there is the pipe method that you can pass your own functions, right? If you have any user-defined functions that you want to use, that’s still possible. But ergonomically, I still have to say that it doesn’t come close to the Tidyverse, but that’s also because of the limitations that Python perhaps gives, right? Because in the end, Python and R are two different languages.

Gregory Kapfhammer 00:23:52 Yeah, that was a persuasive point and thank you for telling us a few details about how R and dplyr connect at least intuitively to Python and Polars. And I gather that one of the key differences is the extensibility associated with R. So, what I want to do now is to focus our conversation on some of those built-in transformations that are provided by Polars that a Python programmer can use out of the box. We’ve hinted already at one or two of them. The first one is the idea of a join and then the second one is the idea of concatenation. Thijs, could you tell us a little bit more what joins and concatenation are in Polars?

Thijs Nieuwdorp 00:24:30 Absolutely. So, I think rarely is it the case that you have one dataset and you can fix all of your problems with it, right? There’s usually you have to combine stuff and that’s what joins are for. So, it’s like the clever way of combining two data frames. So, it’s based on the values in a data frame you can combine them. So earlier I was giving the example of a sales data set and in it you have a store ID and say for example for a store you have other stuff like an employee table or something and you want to figure out what the sales of an employee are to see who to hand the reward of the most valuable employee of the month to, for example. Then in the sales table you won’t find the employees, but there might be like an employee ID. So, you can link on that ID with the employee table and that’s what joins are for.

Thijs Nieuwdorp 00:25:13 So you can have the sales table, that join and you join it with an employee table, and you can mark what values do I match these on? And in this case, you would select the column of employee ID. And there’s also different types of joins that you can do because one of the typical joins that you do is a left join. So, in this case you would keep all the data of the table on the left, in this case the sales table, and you would only enrich that table with the data from the right when it’s available. So, this would mean that for example, if an employee no longer works there is not available in the employee ID table, you will still keep the value from the left. So, you, the sale will be in there, but the employee data is just no, because it can’t be found.

Thijs Nieuwdorp 00:25:55 If you were to do an inner join, you would get just the ones where they connect and if the either in the left table it’s missing or either in the right table, it’s missing, you just drop the row entirely. So that’s something you wouldn’t want in some cases. Or you have like all other types of joins, you have anti-joins, crossed-joins, but that’s a whole another can of worms to get into. So that’s mostly if you want to combine two data sets based on the values inside of them. But there’s also cases where you just want to do a concatenation instead. So, for example, you have data sets that are the same, it’s just a different month, right? So, you have sales data set of January and you want to combine it with February or March to get the Q1 results for example. So, in that case you can do a concatenation because the schema of these two data sets matches and you effectively just duct tape it all together and that’s what a concatenation is for.

Thijs Nieuwdorp 00:26:41 So in this case you would pick like a vertical concatenation where these frames are just added to each other and under the hood there is some schema checking on Polar side. So, if there’s suddenly columns that are different or missing, then you would get errors or you can just allow it to still add it to each other but fill in nulls in that column. Or you can choose to add a column that suddenly appeared because, for example, from February on, we started to suddenly also add extra data on the sales. We have like a new column with a referral, for example, or a voucher that wasn’t there before. Then for the January month that data will be missing, but for the rest it will be added. So, there are some clever options to still combine data sets that way, but it’s not based on the values anymore.

Gregory Kapfhammer 00:27:21 Okay, that was awesome and really helpful. You mentioned the idea of a schema and we’re going to pick up on that later, but really quickly, could you tell our listeners the idea of a Pivot and an un-Pivot operation inside of the Polars API?

Thijs Nieuwdorp 00:27:35 Absolutely. Yeah. So, Pivots I think is a concept that people coming from Excel will definitely know. That’s like the Pivot table is one of the classic transformations that you do to enrich the data that you can get from your sheets. And Polars of course has that too. So, the idea is that there’s different shapes that your data can be in. So, I think it’s also coming from the R side of things. You have the principle of Tidy data and Tidy data describes different shapes that your data can come in. So, you have, for example, a wide shape where it means that for every row — for example, you would have just a person, like one case and every column would be, for example, their grade in high school. So, they have a grade for math, that’s a column, they have a grade for English, for history, and those are all the columns.

Thijs Nieuwdorp 00:28:17 But in the case that they get multiple tests, you would keep adding columns like, and then you get math test one, math test two and that doesn’t feel right. That’s like, that’s not how the data should be represented. So, in that case, you can transform that into more like one row per observation you would call it. So, one row per test score effectively. So in that case you can, I think this case is where it would be to un-Pivot the data, which means to go from a wide format to that long format and then you would get a new data frame with just like one column is the name of the student, one column is the variable name in this case, for example, the name of the course, is it math, is it history? And the last column would be the value, which in this case would be a test score.

Thijs Nieuwdorp 00:28:57 And this creates a very narrow frame, just three columns, but very long with all these observations. And if you want to move it back again, you can Pivot. So, with Pivot, you select which of the columns contains the values that will be turned into columns. So, in this case the course names, you have math, you have history, you have English, all of these are in the values, but they will be turned into columns. And in this new table, all the values will be taken from one of the columns that you mark as having the values, which would be the test scores. And then you go flip it back to that wide format that we have before.

Gregory Kapfhammer 00:29:28 That was fantastic, thank you for all of those details. Go ahead Jeroen.

Jeroen Janssens 00:29:32 Yeah, I just have to smile a little. When Thijs said that he wasn’t sure whether it was Pivot or un-Pivot, I have exactly the same thing. I always have to look up the documentation for that function. And in R, actually, I am a fan in the Tidyverse, they actually solved it by calling it Pivot longer and Pivot wider. So, then you have to do somewhat less thinking, which is which.

Thijs Nieuwdorp 00:29:58 In the office, my desk is next to Ritchie, so I might be able to nudge him here and there to see if we can get the same thing.

Gregory Kapfhammer 00:30:03 Wow, that sounds incredible. By the way, it’s worth noting as we’ve been talking today, we made reference to Tidyverse and Tidy data and I should say there was a paper in the Journal of Statistical Software published in 2014 called Tidy Data. And we’ll link our listeners to that if they want to read more about it. There are also some great websites that say what the Tidyverse is, and I’m sure that people can learn about the Tidyverse and it will help them in many ways to understand the subject of today’s episode, which is Polars. So, thank you for mentioning all of those points. This is a lot of fun. And Jeroen, I wanted to turn to you now because you previously mentioned the idea of declarative and query optimization. So very quickly, what is it that we’re optimizing? And then after you address that, I’ve read that Polars has something called eager and lazy API, and I would love it if you could tell us more details about that idea.

Jeroen Janssens 00:30:57 Yeah, so Polars, especially when you’re working in lazy mode, and I’ll explain that later, it doesn’t just start working the second you give it a command, right? It waits and it builds up a logical plan first, which is kind of like the blueprint of every step that you want to take. Now the query optimizer then looks at that blueprint to find the fastest path to your result. It uses several tricks. One is called predicate push down, and it moves your filters as close to the data source as possible. It also uses a projection push down. So, the engine only reads the specific columns you actually need for your analysis. And this is again why this column or data storage really comes into play. Now by rewriting this plan, that’s how Polars avoids wasting any time or memory on data that isn’t really part of your final answer.

Jeroen Janssens 00:31:50 Now, to come back to that lazy mode, right? The opposite is eager mode. And that is what you would, that most people start using because that is the default and that’s also what most Python developers are actually already used to, right? So, when you’re in eager mode, every line of code is run as soon as you hit enter — I mean, assuming you’re in a console, right? When you’re writing a script that you write enter, you just go to the next line. So that’s also still the mode that I would use most often when I’m analyzing data, I’m just playing around, I’m exploring my data set. I don’t really care how optimized it is; I just want to see results immediately. So that’s eager mode. Now lazy mode is a bit smarter because it waits until the very end to run everything.

Jeroen Janssens 00:32:44 And again, this delay gives the query optimizer a chance to look at your whole plan and do these tricks to make it run faster. And the best part is that they use the exact same syntax apart from you have to make sure that you start off in lazy mode by either using a scan function instead of a read function, right? If you were to, for example, use read_CSV, you read the entire CSV file into memory, when you do scan_CSV, it will start off in lazy mode and again, only read the parts that are actually necessary. And this actually CSV is not the best format for this. Parquet would be much better suited for this because Parquet is just like Arrow, is columnor data storage. At the very end, what you should not forget is that if you want to see those results, you have to materialize them again. So, your lazy frame as it’s called, can be turned into a data frame again by using the collect() method or you use a sync method to stream your data to disc.

Gregory Kapfhammer 00:33:53 I’m tracking with everything that you shared and it was really helpful. I wanted to pick back-up on the fact that you mentioned CSV, because I suspect many of our listeners are used to having their data in a CSV file or a JSON file or maybe an XML file, but most of the time it’s CSV or JSON. So Jeroen, can you tell us a little bit more what are the formats that we can read into a Polars data frame? And then could you expand on the idea of Parquet, because I heard you say that and I’d like our listeners to know a little bit more about it.

Jeroen Janssens 00:34:26 Yeah, what Polars supports so many formats.

Thijs Nieuwdorp 00:34:30 I shared a link with you in the chat. There’s a one page where it has all of them. Oh, right. I was in there today actually to write some documentation.

Gregory Kapfhammer 00:34:37 And I’ll make sure to link to that in the show notes for all of our listeners who are following along. But Jeroen, go ahead. Any quick details you can share?

Jeroen Janssens 00:34:45 Just to give you an idea besides CSV and Parquet, it can read from a database, it can read Excel files. It does use third-party packages for this. It supports Apache Iceberg, JSON, PyArrow datasets, right? And various other formats. But you’re right, the most common ones are, I would say CSV, JSON, right? Especially if you’re interacting with APIs, that’s where you get a lot of JSON from, or read directly from a database or Parquet, right? I mean still Parquet if you, especially when your data gets bigger, Parquet because it compresses the data really helps.

Gregory Kapfhammer 00:35:32 Okay, thanks for mentioning the idea of Parquet compressing data because I think that’s a key point to share. Now, Thijs, there was something else that I wanted to dwell on for a moment. We didn’t specifically yet mention time series data, but my impression is that Polars does a really good job at analyzing data that’s time-series data. Is that the case? And if so, can you develop the idea further?

Thijs Nieuwdorp 00:35:56 Absolutely. So, time series data is quite important. A lot of users of our products are working, for example, in finance where you have a lot of temporal data that you want to process. So, we’ve got a handful of temporal data types that we can use, such as the date, we have the date time, just the time of day. Also, the duration a delta between two points in time. And we have a whole set of methods that you can process this daytime with. So of course, the normal types of join work on this, but one of the problems that you often have is with, daytime these timestamps are very, very precise, like millisecond or even nanosecond level. So often these mismatches, so we have special joins called the join as off, which is a join that is kind of like an inexact match.

Thijs Nieuwdorp 00:36:39 So you can mark two timestamp columns. One of the examples that I often used for this is to say you work with ticker data. So, from the, say Google trades, every once in a while, of course you can write, I can think you can expect the trades quite often. So, there’s a lot of timestamp data and it’s not regular intervals. Just every, anytime there’s a trade that’s made the order book gets updated and like there’s a tick and that timestamp needs to get matched sometimes to, for example, like, there’s another sale that was made. We need to match that with the price, like the last tick that was available. But there’s a mismatch because that sale was not made at a tick. So, what you can do is you can do a join as of, and then you join the sales data to the ticker data and you start searching backwards for the first value that you find and you join the price on a sale that way.

Thijs Nieuwdorp 00:37:24 On top of that, there’s also aggregation like this. So, you have a group by dynamic. So instead of a grouping by, for example, earlier we’re talking about grouping by a store ID, you can create timeframes. So, for example, you can group by a day, and then all the timestamps that fall within that day get grouped together. So instead of grouping on ID numbers or stuff like that, it gets grouped on timestamps and how close they are together. And lastly, there’s also a suite of resampling, up-sampling and, like up-sampling can be combined with fill_null and other imputation methods to create the frame that you want to get to. Last, but not least, we also have the rolling window operation. So, you often see that if you have sensor data — for example, I have a temperature sensor attached outside and it just every minute it gives an update —

Thijs Nieuwdorp 00:38:13 if I want to start working with smoothing out that data because like, for example, if I walk by on my balcony and I walk by the sensor, it gets in the shadow. So suddenly the temperature drops, even though it’s not really a good representation of what’s going on outside. With these rolling methods, you can create like a rolling average and smooth out these peaks. So, for example, even though every minute I’m getting a value, I can get a rolling average of the last hour and what the temperature was, and this way create like a smoothing function, that’s also functionality that’s built in. So, there’s plenty of different things to choose from if you want to work with this temporal data.

Gregory Kapfhammer 00:38:43 Thanks for those details. You told us both about why time series data is beneficial to us and how to analyze it. And in your story, you even told us some gotchas that are associated with analyzing time series data. Now, quickly, I wanted to double click on the idea of categorical data because I know many of our listeners have CSV files or JSON files and they’re expressing the data in those files as strings, but inside of their Polars code that they’re writing in Python, it’s actually supposed to be categorical data. So, could one of you say briefly, what is categorical data and how is that different than string-based data?

Thijs Nieuwdorp 00:39:21 String-based data is variable length, like it can be any type of text, right? So, it can be sentences, it can be tweets, but if I have a data frame with T-shirts in it and T-shirt sizes, if I store those T-shirt sizes, there’s like, we’ve got small, medium, large, maybe some extra sizes around it and that’s it. So, if I have to store a hundred thousand different T-shirts, but there’s only like five different values, then creating this storage of a, that with all the necessary bookmarking pointers to, keep track of how long a string is and where it starts and where it ends. All these, these bookmarking things add overhead, which are, it’s very much necessary if you work with variable length strings, but this is not quite that, right? So instead, you can use a categorical type. So, a categorical type keeps a mapping between an integer value and the string that it represents.

Thijs Nieuwdorp 00:40:11 So in this case you would have, say we have an extra small up until extra-large you have five sizes of t-shirts. So, we just have to save five strings and instead we put the smaller and fixed length integer in there. So, you can even pick an unsigned end of eight bit very small. It can represent plenty of values more than what we need even. And it takes like these eight bits of byte, it takes just this fixed length and this way we can both get more optimal performance because we don’t have to search through the memory buffer because the length is variable. And on top of that we save a lot of space.

Gregory Kapfhammer 00:40:42 Now Jeroen, I wanted to ask you one quick question to make sure we’re on the same page and then I have a follow-on question about something called a user-defined function. So first of all, just to make sure I am clear, we would say that functions like Pivot or un-Pivot, those are what we would call built into Polars, is that the right way to think about it?

Jeroen Janssens 00:41:03 Yeah, they’re part of the Polars package, that’s correct.

Gregory Kapfhammer 00:41:07 Okay. And they’re actually implemented internally in the Rust programming language. Is that also correct?

Jeroen Janssens 00:41:13 That is also correct.

Gregory Kapfhammer 00:41:15 Okay. So now this gets us to the idea of something that’s called a user-defined function. And my understanding is that if I’m a Python programmer, I can make my own UDF and somehow use that inside of my Polars data analysis pipeline. Is that the case? And if so, could you develop that idea a little bit further?

Jeroen Janssens 00:41:35 Yeah, Polars definitely supports UDFs — user-defined functions. But my advice would be to always try and use the built-in expressions first, right? These are essentially the gold standard because they are already optimized in Rust, and they’re built to run as fast as possible. But sometimes you run into situations where you need very specific logic or maybe even need to call on some external package that offers functionality, right? And then plugins are your next best bet. Yeah, so these are yes extensions to Polars. You would also write them in a combination of Rust and Python and they are incredibly fast because you can write your own custom functions and still, you keep that native Rust performance. We used a grade tutorial by Marco Gorelli to build some geo plugin to do things like have her sign distance or checking if a point is in a polygon and, it takes effort to set this up.

Jeroen Janssens 00:42:42 It’s some work, you have to get used to it. Again, I can really recommend this tutorial, but if you are doing this for something that needs to be run more often that’s being run let’s say in into production, then it is worth it because the speed is, that’s a huge payoff. Now when you are working in a notebook, you’re exploring your data, right? Then I would say stick to Python UDFs if you must, right? If the built-in expressions don’t do what you need to do, but they are really your last resort, right? They are much slower and they can break the parallel execution that makes polar so fast.

Gregory Kapfhammer 00:43:22 Okay, so now that you mentioned parallel execution, and we have even said phrases before like optimize and we talked about query plans. I want to move us to the next phase of our conversation and start talking about how we evaluate and optimize the performance of our data analysis that we’re doing with Polars. So in order to start Thijs, I wanted to turn to you. Can you tell us a little bit about what steps our listeners would take if they wanted to set up some kind of repeatable benchmarking and what they would need to measure in terms of the performance of a Python program that uses Polars?

Thijs Nieuwdorp 00:43:58 So to start it off with, I think it’s fair to say that benchmarking is a very, very nuanced ar trying to compare these libraries to each other in a way that’s fair and does it represents a broad SC of different types of queries is quite hard. So, if you want a benchmark for a use case that already makes it easier. So, I think most people will just try to figure out, hey I’ve got this process, I want to just get this done. There’s this query I’ve got in the one package, and I want to test the other package against it. Then it’s relatively easy. There’s a couple of main metrics that you can search for. So, the ones that we generally use is the peak memory usage and the total query runtime.

Thijs Nieuwdorp 00:44:40 If you want to dive a little deeper, like for example in the case that you’re working on cloud instances, like right sizing, these instances can save literally save you money. In that case it also makes sense to take into account the usage of CPU. So, you can see how often is the CPU the bottleneck, how often is at a hundred percent If it’s not, you can scale down the amount of these CPUs. You can see if network bandwidth utilization, if that’s speaking or not, that often indicates that see if you can get larger machines so you can use more of that bandwidth. And in the case that you are serious enough about this, you can even dive into a Polars cloud and Polars on premises where you have a distributed setup available. So, something I recently tested out is that comparing like one large machine versus a set of smaller ones like the set of smaller ones with both twice as fast and half as cheap. So, like 50% saved in terms of cost but also 60% faster. So that’s like there’s all these different knobs you can turn to see how you can optimize performance.

Gregory Kapfhammer 00:45:35 Oh that’s fascinating. Jeroen, one of the things that I read is that you can also run Polars queries and pipelines on Nvidia GPUs. How do GPUs and broadly accelerators fit into the Polars ecosystem?

Jeroen Janssens 00:45:50 Yeah, that’s a really good question because since the book came out, they implemented a streaming engine, so I actually don’t know how they really fit together. Maybe Thijs can say a few things about that in a moment, but what we did do for our book, we had a collaboration with Dell and Nvidia to benchmark Polars running on the CPU versus Polars running on the GPU versus a couple of other packages. What we saw is that especially for larger data sets, because there is still this little overhead that you get for transferring data onto the GPU but for somewhat larger data sets the speedups are tremendous. So Thijs, how does that, do anything about this, the streaming engine and how it interacts with the GPU?

Thijs Nieuwdorp 00:46:42 Yes, so, Polar released a relatively new streaming engine where just like a difference in architecture that’s, the idea is that while we are reading files from disc, it’s already being chopped into what’s called morsels, like micro batches and we already start processing on it. So this, is like an architectural overhaul in terms of how the engine works, but generally with that we see three to seven times speedups compared to the in-memory engine. In fact, in time, we will turn this to the default, but for that we need to have a period where we’re able to catch any edge cases that we didn’t find in testing ourselves. So, it’s that we’re waiting and for a time where that makes the most sense generally, we already recommend turning it on because in most cases it deals very well with most queries. We see that even in the case that something is not implemented yet, it generously falls back for like just that node to the in-memory engine.

Thijs Nieuwdorp 00:47:28 So generally it works. There might be some edge cases where you get a panic and then please let us know and we’ll fix it for you. That’s one of the things that we’ve been working on that sees a lot of improvement. But one of the things where the GPU engine from Nvidia really shines is when you have a lot of joint heavy queries, like a lot of computation, a lot of hashing, a lot of matching, that’s where the GPUs with like its many, many cores shines. That’s also I think one of the benchmarks they did at their release, they saw up to 13 times speedups compared to just running it on the CPU.

Gregory Kapfhammer 00:47:56 Okay, that’s very interesting. So, it’s a 13 times speed up if you run it on a GPU compared to if you run it on a fast CPU. Really interesting.

Jeroen Janssens 00:48:06 It really depends on the type of query that you would like to do. So yes, most queries, so we have this standard set of queries that we benchmark and most of them were faster, some were indeed up to 13 times faster, but at the time there was actually one or two queries that were a little bit slower on the GPU and it all depends on the specific query that you have written and that just goes to say that there is no free lunch also not in this case and you should always check and right because your situation might be different and you may need to approach your problem in a slightly different way.

Gregory Kapfhammer 00:48:48 Yeah, that’s a really good reminder. Now I wanted to talk about an application area and we’re going to do that quickly in a moment. And then once we talk about one specific application area, I’d like to turn our attention to details that are related to data quality and schemas and then also migration. But let’s focus first Jeroen on something that’s related to an application area. I read a blog post that said it’s possible to actually use Polars and parquet files, which you talked about previously, to create something called a text embedding and then to perform something called vector similarity search. Now many of our listeners may know about vector similarity search because they listen to Episode 691, which was about the quadrant vector database, or they might know how to do that with SQLite. But what I have been learning is that you can do something similar with Polars. So, can you tell us a little bit about text embeddings and vector similarity search and how that might work in the context of Polars?

Jeroen Janssens 00:49:48 Yeah, so essentially embeddings are just sequences of numbers, right? They are text, the text embedding is text transformed into an array. And because Polars has the array data type, right, where every sequence of say numbers has to be of the same length, it can work with those values very efficiently. And then to do similarity search that comes down to computing the distance between these high dimensional points, right? Because, because every embedding is a list of points you could also interpret them as being a location in a high dimensional space. Right? So that’s it in a nutshell.

Gregory Kapfhammer 00:50:34 Thanks. That was good. And in fact, it makes me think about the issues related to performance because I know in many cases if I haven’t effectively computed my text embeddings or I picked the wrong distance function for vector similarity search, things can get really slow very quickly. So, I would imagine that having the backend of Polars implemented in Rust is likely a big savings in terms of time overhead when it comes to performing the vector similarity search. Before we move on to talking briefly about schemas and data quality, do either of you want to make any further comments when it comes to this application area or broadly the performance of deployed polar systems in production?

Jeroen Janssens 00:51:16 Well, I think it is really good to mention that Thijs and I, already said we worked at the same company, but we actually worked at the same client for a long time, which was a utilities company in the Netherlands. Very applied. And what we did there was we ported over a very large code base that consisted of Python and R and the code that was in Python was mainly pandas. We ported that over to Polars. So this is has now been years ago, but I do think that this was the very first time that a company had Polars code running in production and the result was that a computation that previously took hours and up to 700 gigabytes of RAM was actually reduced by a lot an order of magnitude. I don’t remember the numbers anymore. We can link to the specific blog post that has these numbers but perhaps more, more interesting is also the way in how we took on this endeavor. And that is piece by piece we translated Python functionality, mainly written in pandas to Polars and all the while as we were doing this benchmarking along the way.

Gregory Kapfhammer 00:52:40 Thanks, that was incredible and very helpful. So, what you were doing is taking code that was R and then Python and pandas and converting it to Polars. And I remember reading about this case study and so Thijs, I wanted to turn to you quickly. I think there was the idea in that case study that you should avoid row wise operations. Now building on what we have already talked about in the episode, can you tell our listeners why you should avoid row wise operations, especially if you care about performance and you’re using Polars?

Thijs Nieuwdorp 00:53:11 I can elaborate a little bit more on the case that we were doing. So, the part of utilities network that we were focused on is like the super local electricity network. So, it’s literally like the copper wire that goes from your house to one of these local transformers. And with the energy transition, well on its way with people putting solar panels on their roof, putting Teslas in front of that door, the copper wires that once only used a heat like an oven or a water cooker, now suddenly had to charge Teslas and it’s not something they were prepared for, right? So, our assignment was to try and simulate and anticipate what demand on the network would be and which parts of the network of the 35,000 subnets that we were working with would require upgrades first. So, we can manage this situation well.

Thijs Nieuwdorp 00:53:55 Originally we had this R and pandas combo. I think that originally was prototyped in R on a massive server. So, performance was not something that was top of mind. But over time, because we wanted to go to these 35,000 subnets, we had to improve both the way the software was written, but also the tools we were using. Coming from pandas, row wise operations are not a problem. It’s built on MyPy. Generally, it’s relatively optimized for that, but that’s something that for Polars doesn’t quite work because it’s got these, this columnar storage like having, thousands of columns very quickly get less optimal, right? So that’s some of the things we had to optimize is to suddenly start thinking in a different mindset. And that’s something that was ready for Polars with parts of the transformations that we were doing. It is relatively easy to swap the pandas or swap from pandas to Polars and swap it back.

Thijs Nieuwdorp 00:54:40 And that’s how we started with the, I think originally like trying to convince the client team like, hey, we’ve got this Polars tool that is coming from Xomnia and we want to implement it in your stack. It is not the easiest thing to do. So, I think it’s just like one week where we got some freedom to innovate on the project. I took one part of the pipeline’s, relatively straightforward, it is like 30 seconds of runtime, and it was just, okay, grab data frame out of the Cloud, added it to the existing data and then applied some business rules. So, in pandas that took about 30 seconds and then I transferred that to Polars, and we got it down to 1.2 seconds and that’s, I think when some people started, okay so this Polar thing may actually be something that could really help us out here. And over time with patches like this, we started, swapping out the performance critical parts first until one point we swapped it all over to Polars entirely. And that’s how in the end, I actually, I was looking up the numbers in the background, what originally would’ve cost about 150,000 for the entire calculation. We brought that down to 3.5 K euros, which is well under the budgeted 5K Euro. So, a total reduction in cost of 98%.

Gregory Kapfhammer 00:55:44 Wow. Thanks for sharing that story. That’s incredible. So now you mention all of these performance improvements, and I think that’s a great story to tell Thijs. Jeroen, can you tell us a little bit about what you did to ensure the correctness of your system? So, were there any testing techniques that you developed or were there any debugging practices that you followed when you were doing this transfer of the system from Python to Polars?

Thijs Nieuwdorp 00:56:08 So because we had to keep the correctness, one of the things we did is the parts of the pipeline. We created unit tests around that. And I kind of heard like this input output check, right? Just treat the pipeline as a black box and input and output had to remain the same. And that’s something that even though in Polars we might have chosen for some different ways of transforming it, in the end the result had to be the same. And with smaller, smaller sample data sets, it’s relatively easy to get that done in unit tests, I think one of the tools we found at the time is the, from REPR, like the string representation from REPR function, you can put a piece of string in your code and it’s literally just, if you print the data frame, you can just copy paste that, put it in a code and that constructs a data frame in Polars.

Thijs Nieuwdorp 00:56:51 So it’s very easy to keep track of which kind of data samples you’re working with, very easy to edit. And it was functionality like that combined with asserting frames frame equal to seeing if the match was still there. And that’s just something that we kept on checking. So, in case we accidentally bumped something over or it’s very easy to accidentally transform something in a wrong way, especially when the paradigms behind the tools you’re using are different. But this definitely saved the, I think in the end, ended up saving quite a lot of time.

Gregory Kapfhammer 00:57:18 Jeroen, is there anything you wanted to add to the story?

Jeroen Janssens 00:57:21 Yeah, yeah. I wanted to say that it might be good to point out to others who may want to do something similar, right? They may have an existing pipeline written in some language. Let us choose pandas again, Python and pandas. And if they wanted to convert that into Polars, it is important to realize that you cannot do this on a line-by-line basis because the two are just so different. And that is also why Thijs talked about the input and output rather than worrying about the individual transformations or operations that pandas were doing, we just looked at, okay, what did it start with? What does it end with and how can we get there, from A to B using something else?

Gregory Kapfhammer 00:58:10 Thanks for that insight. I know our listeners have been following along and they probably have an interest in learning more about Polars because of course we haven’t covered all of the details. Could the two of you share a little bit with our listeners about how they can learn about your book or learn more about the parts of the Polars ecosystem?

Jeroen Janssens 00:58:29 Yeah, so we have a website, the book has a website, it’s.com and on it you can get the first chapter for free, and you can get to other interesting resources from that. It’s also how you can get in touch with it, Thijs or myself. So yes, Polarsguide.com, that’s interesting when you want to know more about the book. And then there are of course also there is the Polars documentation itself, which is excellent.

Thijs Nieuwdorp 00:58:59 The Polars website I think is the best hub too, that connects everything together. So, if you go to Polar.rs, unfortunately Polars.com has already taken and we have to buy it for 50K, so that is not going to work. So, if you go to Polar.rs, that is where you can find way more information here. You can find your connections to the user guide, which gives a more detailed explanation of how to work with these tools with some examples. You can find the API reference that has complete documentation on all the expressions that there are. I believe during the writing of the book at one point, Jeroen found out that there are over three hundred expressions even so quite regularly come on that website as well. And you can also find more on the offerings that Polars had because we recently started a cloud product because we know that there is a demand for distributed engines as well. And that’s something that we are heavily building on right now.

Gregory Kapfhammer 00:59:47 All right. This has really been an engaging and informative conversation and as we draw the episode to a conclusion, I would like to turn to each of you individually. Is there either a new topic that we have not covered that you wanted to very briefly highlight or do you have a call to action for our listeners so that they can get more involved with Polars and the polar ecosystem in data engineering?

Jeroen Janssens 01:00:09 I would say that great way to become part of the Polars community is to join the Discord server. It’s very welcoming no matter what your level is, and you can ask questions there, interact with others and people are helpful and then give it a spin.

Gregory Kapfhammer 01:00:27 Alright, that was awesome. Thijs, anything else you wanted to share in conclusion?

Thijs Nieuwdorp 01:00:31 Yeah, so the main thing from the Polar side that we’ve been working on is really becoming a unified API for not only being like a small single node data frame library, but to also go all the way to scaling up the big data. And that is something that we’re heavily investing in now. So like I said, there’s the Power Potters cloud side of things, and even on Premises, if you want to run it on your own Kubernetes cluster so your data stays safe wherever you want to keep it, we’re building heavily on products that can scale all the way there and are already defeating Spark in terms of performance with managed Spark solutions. So, we strive to keep being the performance leader on any skill at this point.

Gregory Kapfhammer 01:01:07 Thank you. That sounds super cool. We’ll make sure to link all of our listeners to everything that we’ve mentioned in the episode. So, you’re welcome to check the show notes. At this point, Thijs and Jeroen, thank you so much for the conversation. It was great to chat with you both today.

Thijs Nieuwdorp 01:01:21 Thank you for having us. It was great.

Jeroen Janssens 01:01:22 Thank you.

Gregory Kapfhammer 01:01:23 Hey, you’re welcome. And this is now Gregory Kapfhammer signing off for Software Engineering Radio.

[End of Audio]

Join the discussion

More from this show