<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Adam Cseresznye</title><description>Mass spectrometry scientist, consultant and hobby scientific software developer for analytical chemistry and biotech applications. Based in San Francisco.</description><link>https://adamcseresznye.github.io</link><item><title>🏆🥉3rd place at Kaggle&apos;s Classification with a Tabular Vector Borne Disease Dataset</title><link>https://adamcseresznye.github.io/posts/kaggle</link><guid isPermaLink="true">https://adamcseresznye.github.io/posts/kaggle</guid><pubDate>Sun, 04 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Greetings everyone! I am thrilled to announce that I have achieved a significant milestone in my data science journey. I am delighted to share that I secured the 3rd position in Kaggle&apos;s &quot;Classification with a Tabular Vector Borne Disease Dataset&quot; competition, surpassing 931 talented contenders! If you&apos;re interested in learning more about my approach to the competition, I invite you to check out my detailed solution on the &lt;a href=&quot;https://www.kaggle.com/competitions/playground-series-s3e13/discussion/406409&quot;&gt;Kaggle discussion page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;230604-Kaggle-img1.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In this exciting competition, the aim was to predict the three most probable diseases out of a total of 11 vector-borne diseases, including well-known names such as Zika, Yellow Fever, Malaria, and more. Our task involved analyzing the provided symptoms and using the MPA@3 evaluation metric for accurate predictions.&lt;/p&gt;
&lt;p&gt;My journey in the world of Kaggle has been a relatively short one, with this competition marking my third participation. To give you some context, let me share a bit about my previous placements:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;230604-Kaggle-img2.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Reaching this point has been a long and rewarding endeavor. Over the past two years, I have been studying this topic in my free time while also pursuing a full-time Ph.D. research. It has been an exciting balancing act!&lt;/p&gt;
&lt;p&gt;Participating in Kaggle competitions has been an invaluable learning experience for me. It has allowed me to apply my knowledge, explore new techniques, and connect with fellow data enthusiasts. I am deeply grateful to the organizers of Kaggle for providing a platform that fosters growth and healthy competition.&lt;/p&gt;
&lt;p&gt;While this achievement brings a sense of accomplishment, I am already diving into new competitions and expanding my skills. Currently, I am particularly interested in reproducible data and developing robust architectures for machine learning projects to enhance code reproducibility. In fact, I am considering writing a blog on this often overlooked topic.&lt;/p&gt;
&lt;p&gt;Stay tuned for more :)&lt;/p&gt;
&lt;p&gt;#DataScience #KaggleCompetition #VectorBorneDiseases #MachineLearning #ReproducibleData&lt;/p&gt;
</content:encoded><author>Adam Cseresznye</author></item><item><title>Top 10 things I learned from the book Effective Pandas by  Matt Harrison</title><link>https://adamcseresznye.github.io/posts/top-10-things-i-learned-from-effective-pandas</link><guid isPermaLink="true">https://adamcseresznye.github.io/posts/top-10-things-i-learned-from-effective-pandas</guid><pubDate>Wed, 02 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://store.metasnake.com/effective-pandas-book&quot;&gt;Effective Pandas by Matt Harrison&lt;/a&gt; is a guide to the Pandas library, a powerful Python tool for data manipulation and analysis. The book covers a wide range of topics, from basic data structures to advanced techniques for data cleaning, transformation, and visualization.&lt;/p&gt;
&lt;p&gt;I have found Effective Pandas to be a captivating and thought-provoking read. The book offers a genuinely unique take on data wrangling, putting a great emphasis on the utility of &lt;em&gt;chaining methods&lt;/em&gt; and utilizing the &lt;em&gt;lambda function&lt;/em&gt;. I have found these ideas to be so useful and practical that I have revisited the book multiple times just to make sure I keep them fresh in my memory. I must have read the book from back to back at least 3-4 times.&lt;/p&gt;
&lt;p&gt;In this article, I will share the top 10 (+1) things I learned from Effective Pandas. These are the concepts and techniques that I found to be the most useful and practical.&lt;/p&gt;
&lt;p&gt;We will use the &lt;a href=&quot;https://www.kaggle.com/datasets/abhijitdahatonde/real-world-smartphones-dataset?select=smartphones.csv&quot;&gt;Real World Smartphone&apos;s Dataset&lt;/a&gt; by Abhijit Dahatonde from Kaggle. Let&apos;s get to it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
pd.set_option(&apos;display.max_columns&apos;, None)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Load the dataset&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;df=pd.read_csv(&apos;smartphones.csv&apos;)
# some info about the dataframe, such as dimensions and dtypes of columns
df.info()
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Tip #1: Use &amp;lt;code&amp;gt;pd.assign&amp;lt;/code&amp;gt; more extensively&lt;/h1&gt;
&lt;p&gt;The &amp;lt;code&amp;gt;pd.assign&amp;lt;/code&amp;gt; method in Pandas is a very powerful tool that can be used to create new columns, modify existing columns, or both. It is a very versatile method that can be used in a variety of ways.&lt;/p&gt;
&lt;p&gt;One of the most important benefits of using the assign method is that it can be incorporated into method chaining. This means that you can chain multiple assign methods together to create a more concise and readable code. Another benefit of using the assign method is that it completely sidesteps the infamous &amp;lt;code&amp;gt;SettingWithCopyWarning&amp;lt;/code&amp;gt;. This warning is often triggered when you try to modify an existing column in a DataFrame. However, the assign method creates a new DataFrame, so there is no need to worry about this warning.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem statement:&lt;/strong&gt; Let&apos;s say we would like to capitalize the brand names located in the &amp;lt;code&amp;gt;brand_name column&amp;lt;/code&amp;gt; as well as calculate the Pixels Per Inch (PPI). PPI can be calculated following the equation described by the &lt;a href=&quot;https://en.wikipedia.org/wiki/Pixel_density&quot;&gt;Pixel density&lt;/a&gt; page on Wikipedia.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(df
 .assign(brand_name=lambda df: df.brand_name.str.capitalize(), # capitalizes the brand names 
         PPI=lambda df: (np.sqrt(np.square(df.resolution_height) + np.square(df.resolution_width))
                         .div(df.screen_size)
                         .round(1)
                        )
        )
 .loc[:, [&apos;brand_name&apos;,&apos;model&apos;,&apos;PPI&apos;]]
 .sort_values(by=&apos;PPI&apos;,ascending=False)
 .head(5)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Tip #2: Simplify the management of multiple if-else conditions using &amp;lt;code&amp;gt;np.select&amp;lt;/code&amp;gt;&lt;/h1&gt;
&lt;p&gt;If our goal is to incorporate if-else logic seamlessly into our code, we can effortlessly achieve this using either &amp;lt;code&amp;gt;pd.mask&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;pd.where&amp;lt;/code&amp;gt;. Yet, what approach should we adopt when we need to evaluate multiple conditions instead of just two? In such situations, we have two options: we can either employ successive &amp;lt;code&amp;gt;pd.mask&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;pd.where&amp;lt;/code&amp;gt; calls, or we can take advantage of the &amp;lt;code&amp;gt;np.select&amp;lt;/code&amp;gt; function as an alternative solution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem statement:&lt;/strong&gt; We want to identify the top 3 and top 5 most popular processor brands in smartphones. To do this, we will first create two lists, one for the top 3 brands and one for the top 5 brands. Any processor brand that is not in either of these lists will be categorized as &quot;Other&quot;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Let&apos;s create the two lists that contain the top3 and top5 brand names
top3=df.processor_brand.value_counts().head(3).index
top5=df.processor_brand.value_counts().head(5).index
print(f&apos;Top 3 most popular processors: {top3.tolist()}&apos;)
print(f&apos;Top 5 most popular processors: {top5.tolist()}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&apos;&apos;&apos;
Here&apos;s an example that employs two successive pd.where calls:
In the first pd.where call, it checks whether the brand is in the top 3; if not, it assigns the label &quot;Top5&quot; to it.
Then, in the second call, it checks if the value is in the top 5; if not, it appends the category &quot;Other&quot;.
As you can see, the logic can become intricate and difficult to grasp, especially when dealing with numerous conditions, 
making the code cumbersome and hard to manage.
&apos;&apos;&apos;
(df
 .assign(frequency=lambda df: df.processor_brand
         .where(df.processor_brand.isin(top3), other = &apos;Top5&apos;)
         .where(df.processor_brand.isin(top5), other = &apos;Other&apos;)
        )
 .loc[:, &apos;frequency&apos;]
 .value_counts()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&apos;&apos;&apos;
 Now Let&apos;s see np.select!
 It simplifies the process significantly. By providing a list of conditions we want to evaluate and their 
 corresponding values if the condition evaluates to True, we can handle multiple conditions effortlessly. 
 Additionally, we can specify a default value if none of the conditions evaluates to True, making the code 
 much more straightforward and easier to manage. 
&apos;&apos;&apos;
(df
 .assign(frequency=lambda df: np.select(condlist=[df.processor_brand.isin(top3), df.processor_brand.isin(top5)],
                                        choicelist=[df.processor_brand,&apos;Top5&apos;],
                                        default=&apos;Other&apos;
                                       )
        )
 .loc[:, &apos;frequency&apos;]
 .value_counts()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Tip #3 Filter rows or columns with the combination of &amp;lt;code&amp;gt;pd.loc&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;lambda&amp;lt;/code&amp;gt;&lt;/h1&gt;
&lt;p&gt;Some experienced Pandas users might consider the following concept trivial, but it was an eye-opener for me after reading the book. It turns out that combining &amp;lt;code&amp;gt;pd.loc&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;lambda&amp;lt;/code&amp;gt; (or any custom functions) allows us to filter both rows and columns, depending on our specific needs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem statement:&lt;/strong&gt; We are interested in identifying phones with a battery capacity greater than 5000mAh.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(df
 .loc[lambda df: df.battery_capacity.gt(5000),[&apos;model&apos;, &apos;battery_capacity&apos;]] # here we use pd.gt() to select values greater than 5000
 .sort_values(by=&apos;battery_capacity&apos;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Tip #4 Rename multiple columns effortlessly with &amp;lt;code&amp;gt;rename&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;replace&amp;lt;/code&amp;gt;&lt;/h1&gt;
&lt;p&gt;OK. This is a big one for me. I used this one multiple times already. The title pretty much says it all. Let&apos;s say our column names contain spaces, which makes column selection by attribute access pretty much impossible.  Now, what do we do?
Well...Let&apos;s see.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem statement:&lt;/strong&gt; We would like to remove all the underscores from our column names.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.columns # original column names for reference
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# column names after replacing underscores
(df
 .rename(columns = lambda x: x.replace(&apos;_&apos;, &apos;&apos;))
 .columns
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Tip #5: Use &amp;lt;code&amp;gt;pd.clip&amp;lt;/code&amp;gt; to easily remove outliers&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;# First, we&apos;ll identify the phone brands with the most number of handsets present in our dataset.&quot;
top10_brand_names = (df
                     .brand_name
                     .value_counts()
                     .head(10)
                     .index
                     .tolist()
                    )
print(top10_brand_names)

# Then we will sort them based on median price
top10_brand_names_ordered = (df
                             .loc[lambda x: x.brand_name.isin(top10_brand_names),[&apos;brand_name&apos;, &apos;price&apos;]]
                             .groupby(&apos;brand_name&apos;)
                             .median()
                             .sort_values(by=&apos;price&apos;)
                             .index
                             .to_list()
                            )
print(top10_brand_names_ordered)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;fig, axs = plt.subplots(1,2, figsize=(10,5))

# For reference, this is what our box plot looks if we leave in the outlier values
(df
 .loc[lambda x: x.brand_name.isin(top10_brand_names),[&apos;brand_name&apos;, &apos;price&apos;]] # filter rows based on top10_brand_names and select columns
 .pivot(columns=&apos;brand_name&apos;,values=&apos;price&apos;) # pivot to get the brand names on the x axis later on
 .loc[:, top10_brand_names_ordered] # order the columns based on median price
 .plot
 .box(title=&apos;Top 10 most popular smartphone brands \n -outlier values included-&apos;, 
      rot=90,
      ax=axs[0]
     )
)

(df
 .loc[lambda x: x[&apos;brand_name&apos;].isin(top10_brand_names), [&apos;brand_name&apos;, &apos;price&apos;]]
 .pivot(columns=&apos;brand_name&apos;, values=&apos;price&apos;)
 .loc[:, top10_brand_names_ordered]
 .pipe(lambda df: df.assign(**{col : df[col].clip(lower=df[col].quantile(0.05), # this is called dictionary unpacking
                                                  upper=df[col].quantile(0.95))
                              for col in df.columns}))
 .plot
 .box(title=&apos;Top 10 most popular smartphone brands \n -only values between 5th and 95th percentiles included-&apos;,
      rot=90,
      ax=axs[1]
     )
)
axs[0].set(ylabel=&apos;Price in local currency&apos;)
plt.tight_layout()
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Tip #6: Find corrupted entries with &amp;lt;code&amp;gt;str.extract&amp;lt;/code&amp;gt;&lt;/h1&gt;
&lt;p&gt;How often have you encountered the situation where, for some reason, a column that is expected to only contain numerical values displays &amp;lt;code&amp;gt;object&amp;lt;/code&amp;gt; as its &amp;lt;code&amp;gt;dtype&amp;lt;/code&amp;gt;? This often indicates the presence of some string values mixed within the column. It would be beneficial to promptly identify all the erroneous values, correct them, and proceed with our analysis smoothly. Let&apos;s explore what we can do in such scenarios.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem statement:&lt;/strong&gt; We would like to identify any cells in a specific column that contain non-numerical values.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df_bad_values = df.copy(deep=True) # let&apos;s prepare a copy of the original dataframe 

# let&apos;s modify some of the values in the price column randomly:
df_bad_values.loc[np.random.randint(0, high=df_bad_values.shape[0], size=10), &apos;price&apos;] = &apos;.&apos;
df_bad_values.loc[np.random.randint(0, high=df_bad_values.shape[0], size=10), &apos;price&apos;] = &apos;-&apos;
df_bad_values.loc[np.random.randint(0, high=df_bad_values.shape[0], size=10), &apos;price&apos;] = &apos;*&apos;
df_bad_values.info() # the modified dataframe&apos;s price column now returns object as dtype
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# let&apos;s find the corrupted values easily:
(df_bad_values
 .price
 .str.extract(r&apos;([^a-zA-Z])&apos;) # returns NON-matching alphabetical characters
 .value_counts()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Tip #7: Sort values based on the &amp;lt;code&amp;gt;key&amp;lt;/code&amp;gt; parameter&lt;/h1&gt;
&lt;p&gt;The &amp;lt;code&amp;gt;pd.sort_values&amp;lt;/code&amp;gt; function surprised me with its versatility. Previously, I had only used its &amp;lt;code&amp;gt;by&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;axis&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;ascending&amp;lt;/code&amp;gt; parameters for sorting. However, Matt&apos;s book introduced me to its &amp;lt;code&amp;gt;key&amp;lt;/code&amp;gt; parameter, which allows us to apply any function to sort the values. The only constraint is that the function must return a &amp;lt;code&amp;gt;Series&amp;lt;/code&amp;gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem statement:&lt;/strong&gt; For whatever reason, we would like to sort the phone model names in ascending order based on their second letter.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(df
 .iloc[:, 1:3]
 .sort_values(by=&apos;model&apos;,
              key = lambda x: x.str[1],
              ascending = True
             )
 .head(10)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Tip #8: Reference an existing variable inside &amp;lt;code&amp;gt;pd.query&amp;lt;/code&amp;gt; with @&lt;/h1&gt;
&lt;p&gt;This resembles Tip #4, as it&apos;s a technique I frequently use. Since reading Matt&apos;s book, I have started using &amp;lt;code&amp;gt;pd.query&amp;lt;/code&amp;gt; extensively to filter rows based on values, instead of relying on &amp;lt;code&amp;gt;.loc&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;.iloc&amp;lt;/code&amp;gt;. In case you choose to adopt &amp;lt;code&amp;gt;pd.query&amp;lt;/code&amp;gt; as well, it&apos;s essential to be aware of its capability to use &quot;@&quot; to reference variables in the environment. This feature enhances its flexibility and makes it even more convenient to apply in various data filtering scenarios.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem statement:&lt;/strong&gt; Our objective is to identify phones that meet three specific criteria: being priced below the average market price, having more processor cores than the average, and possessing a battery capacity greater than the average.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;average_price=df.price.mean()
average_cores=df.num_cores.mean()
average_battery=df.battery_capacity.mean()

(df
 .query(&quot;(price &amp;lt;= @average_price) and (num_cores &amp;gt;= @average_cores) and (battery_capacity &amp;gt;= @average_battery)&quot;)
 .iloc[:,:3]
 .sort_values(by=&apos;price&apos;)
 .head()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Tip #9: Gain more insights by using &amp;lt;code&amp;gt;style.background_gradient&amp;lt;/code&amp;gt;&lt;/h1&gt;
&lt;p&gt;Being a visual creature, I often struggle to comprehend data quickly just by examining the raw table alone. Fortunately, with the help of &amp;lt;code&amp;gt;style.background_gradient&amp;lt;/code&amp;gt;, similar to the &amp;lt;code&amp;gt;heatmap&amp;lt;/code&amp;gt; function in the &amp;lt;code&amp;gt;seaborn&amp;lt;/code&amp;gt; library, we can represent cells, in terms of color gradient, based on their values. This enables us to identify trends and patterns in our data swiftly, making data analysis more intuitive and insightful.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem statement:&lt;/strong&gt; Our goal is to identify the overall trends related to key descriptors found among the Top 10 smartphone brands, aiming to determine which brand offers the most value for our money.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(df
 .query(&quot;brand_name.isin(@top10_brand_names_ordered)&quot;) # filter rows based on top10 brands
 .groupby([&apos;brand_name&apos;,])
 [[&apos;avg_rating&apos;, &apos;processor_speed&apos;, &apos;ram_capacity&apos;, 
   &apos;screen_size&apos;, &apos;battery_capacity&apos;, &apos;price&apos;]]
 .mean()
 .sort_values(by=&apos;avg_rating&apos;)
 .transpose()
 .rename(columns=str.capitalize) # capitalize brand names
 .style
 .set_caption(&quot;Key descriptors of the Top 10 smartphone brands&quot;)
 .format(precision=1)
 .background_gradient(cmap = &apos;vlag&apos;, axis = 1)
 .set_table_styles([
    {&apos;selector&apos;: &apos;td&apos;, &apos;props&apos;: &apos;text-align: center;&apos;},
     {&apos;selector&apos;: &apos;caption&apos;,&apos;props&apos;: &apos;font-size:1.5em; font-weight:bold;&apos;}
     ,]
 )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Tip #10: Use &amp;lt;code&amp;gt;pd.pipe&amp;lt;/code&amp;gt; to include any functions in our chain&lt;/h1&gt;
&lt;p&gt;One of the most valuable lessons I learned from Effective Pandas is the importance of arranging my code in a chain. Although it may feel somewhat restrictive at first, once you overcome the initial hurdles, you&apos;ll realize that your code becomes more readable and easier to understand. The need to invent unique names for temporary variables is completely eliminated, making coding a much happier experience.&lt;/p&gt;
&lt;p&gt;In the chaining world, you often find yourself wanting to use various functions that are not explicitly designed for chaining. However, there&apos;s good news! You can still achieve this. The  &amp;lt;code&amp;gt;pd.pipe&amp;lt;/code&amp;gt; function comes to the rescue, allowing you to use any function as long as it returns a Series or DataFrame. It&apos;s a flexible solution that empowers you to seamlessly integrate different functions into your chaining workflow, making your data manipulation more efficient and enjoyable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem statement:&lt;/strong&gt; We aim to visualize the impact of RAM capacity on user satisfaction. To achieve this, we will utilize the &amp;lt;code&amp;gt;sns.lmplot&amp;lt;/code&amp;gt; function, which plots the data and corresponding regression models for the Top 5 phone brands.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;top5_brand_names_ordered = df.brand_name.value_counts().head().index

with sns.axes_style(&quot;darkgrid&quot;):
    g = (df
         .query(&quot;brand_name.isin(@top5_brand_names_ordered)&quot;) # filter rows based on top10 brands
         [[&apos;brand_name&apos;, &apos;avg_rating&apos;, &apos;ram_capacity&apos;]]
         .assign(brand_name=lambda df: df.brand_name.str.capitalize())
         .rename(columns={&apos;brand_name&apos;:&apos;Brand name&apos;})
         .pipe(lambda df: sns.lmplot(data=df,
                                     x=&apos;ram_capacity&apos;,
                                     y=&apos;avg_rating&apos;,
                                     hue=&apos;Brand name&apos;,
                                     # height=4,
                                     # aspect=1.2
                                    )
              )
        )

    g.set(title=&apos;Customer satisfaction correlates with RAM capacity&apos;, 
          xlabel=&apos;RAM capacity&apos;,
          ylabel=&apos;User rating&apos;
         )
plt.tight_layout()
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Tip #10 + 1: Use the &quot;margin&quot; parameter of &amp;lt;code&amp;gt;pd.crosstab&amp;lt;/code&amp;gt; to easily calculate row/column subtotals&lt;/h1&gt;
&lt;p&gt;Despite primarily using the pandas &amp;lt;code&amp;gt;groupby&amp;lt;/code&amp;gt; function for data aggregation, the &amp;lt;code&amp;gt;pd.crosstab&amp;lt;/code&amp;gt; function has an enticing feature: the &amp;lt;code&amp;gt;margin&amp;lt;/code&amp;gt; parameter. This option enables us to effortlessly calculate subtotals across rows and columns. Moreover, by normalizing our data, we can gain even more intuition about the questions we want to answer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem statement:&lt;/strong&gt; Our objective is to evaluate how RAM capacity impacts user satisfaction across the Top 5 brands. Additionally, we will normalize our data to compare values comprehensively across the entire dataset.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(df
 .query(&quot;brand_name.isin(@top5_brand_names_ordered)&quot;) # filter rows based on top10 brands
 .assign(brand_name=lambda df: df.brand_name.str.capitalize())
 .pipe(lambda df: pd.crosstab(index=df[&apos;ram_capacity&apos;],
                              columns=df[&apos;brand_name&apos;],
                              values=df[&apos;avg_rating&apos;],
                              aggfunc=&apos;mean&apos;,
                              margins=True,
                              normalize=&apos;all&apos;
                             )
      )
 .mul(100)
 .round(1)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I hope this article has convinced you to pick up Matt Harrison&apos;s &lt;a href=&quot;https://store.metasnake.com/effective-pandas-book&quot;&gt;Effective Pandas&lt;/a&gt;! There are plenty more exciting ideas in the book beyond the Top 10 I&apos;ve shared here (I didn&apos;t even get into the fascinating time series part!). I hope you found these insights helpful and inspiring.&lt;/p&gt;
&lt;p&gt;Happy coding 🐼💻🚀&lt;/p&gt;
</content:encoded><author>Adam Cseresznye</author></item><item><title>How to use the Lets-Plot library by JetBrains</title><link>https://adamcseresznye.github.io/posts/lets-plot</link><guid isPermaLink="true">https://adamcseresznye.github.io/posts/lets-plot</guid><pubDate>Sat, 05 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://upload.wikimedia.org/wikipedia/en/thumb/0/08/JetBrains_beam_logo.svg/1200px-JetBrains_beam_logo.svg.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;When I embarked on my data science journey,  due to my academics background I quickly gravitated towards the &lt;a href=&quot;https://www.r-project.org/&quot;&gt;R programming language&lt;/a&gt;. Like many R novices, I began with Hadley Wickham&apos;s &lt;a href=&quot;https://r4ds.hadley.nz/&quot;&gt;R for Data Science&lt;/a&gt; book which introduced me to the wonderful &lt;a href=&quot;https://ggplot2.tidyverse.org/&quot;&gt;ggplot2&lt;/a&gt; library. As my interest in machine learning grew, I made the switch to Python. Nowadays, for most of my data plotting needs, I rely mainly on &lt;a href=&quot;https://matplotlib.org/&quot;&gt;matplotlib&lt;/a&gt; or &lt;a href=&quot;https://seaborn.pydata.org/&quot;&gt;seaborn&lt;/a&gt;. Though I love these libraries, their multiple ways of accomplishing the same tasks can be a bit cumbersome and challenging to learn at first.&lt;/p&gt;
&lt;p&gt;That&apos;s why in this article, I&apos;m excited to introduce you to the &lt;a href=&quot;https://lets-plot.org/index.html#&quot;&gt;Lets-Plot&lt;/a&gt; library by JetBrains. It is the closest you can get to ggplot&apos;s syntax while using Python. While some traditional Python users might find the syntax a bit unfamiliar initially, I&apos;m here to make a case for this fantastic library and hopefully inspire you to incorporate it into your day-to-day data science activities.&lt;/p&gt;
&lt;p&gt;To showcase (some of) the key features of Lets-Plot, we will be utilizing the &lt;a href=&quot;https://gist.githubusercontent.com/slopp/ce3b90b9168f2f921784de84fa445651/raw/4ecf3041f0ed4913e7c230758733948bc561f434/penguins.csv&quot;&gt;penguins dataset&lt;/a&gt; 🐧 from Github.&lt;/p&gt;
&lt;p&gt;Without further ado, let&apos;s dive right in and discover the power and beauty of Lets-Plot! 🐍📊&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Installation
# pip install lets-plot 
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Import libraries
import numpy as np
import pandas as pd
from lets_plot import *
from lets_plot.mapping import as_discrete
LetsPlot.setup_html()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;address=&apos;https://gist.githubusercontent.com/slopp/ce3b90b9168f2f921784de84fa445651/raw/4ecf3041f0ed4913e7c230758733948bc561f434/penguins.csv&apos;
df=pd.read_csv(address)
df.head()
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Syntax Similarities: &amp;lt;code&amp;gt;Lets-Plot&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ggplot2&amp;lt;/code&amp;gt;&lt;/h1&gt;
&lt;p&gt;For our first exercise, I thought it would be beneficial to replicate a basic plot inspired by Hadley&apos;s book. When comparing my code here with &lt;a href=&quot;https://r4ds.hadley.nz/data-visualize#sec-adding-aesthetics-layers&quot;&gt;the one presented by him&lt;/a&gt;, you&apos;ll notice that there is very little difference between the two. The syntax is nearly identical, making it a smooth transition from ggplot to Lets-Plot.&lt;/p&gt;
&lt;p&gt;Now, let&apos;s take a closer look at the code. In the &amp;lt;code&amp;gt;ggplot&amp;lt;/code&amp;gt; function, we define the &amp;lt;code&amp;gt;DataFrame&amp;lt;/code&amp;gt; we&apos;ll be working with, and the aesthetic mappings are set at the &lt;em&gt;global level&lt;/em&gt;. We assign the values for the x and y axes, as well as the &amp;lt;code&amp;gt;color&amp;lt;/code&amp;gt; argument, which groups the data based on the categorical variable representing the three different penguin species: Adelie, Gentoo, and Chinstrap. This color parameter is quite similar to seaborn&apos;s &amp;lt;code&amp;gt;hue&amp;lt;/code&amp;gt;, making it easy for those familiar with seaborn to adapt to Lets-Plot seamlessly.&lt;/p&gt;
&lt;p&gt;After the &amp;lt;code&amp;gt;ggplot()&amp;lt;/code&amp;gt; function sets the global aesthetic mappings, the &amp;lt;code&amp;gt;geom_point()&amp;lt;/code&amp;gt; function comes into play and draws the points defined by the x and y parameters, effectively creating a scatter plot. These points represent the data points from the penguins dataset, with x and y coordinates corresponding to the specified variables.&lt;/p&gt;
&lt;p&gt;Additionally, we enhance the plot by using &amp;lt;code&amp;gt;geom_smooth(method=&apos;lm&apos;)&amp;lt;/code&amp;gt;, which adds a smoothed conditional mean. The &amp;lt;code&amp;gt;lm&amp;lt;/code&amp;gt; method stands for &apos;linear model,&apos; indicating that the smoothing is based on a linear regression. This smoothed line helps reveal trends and patterns in the data, making it easier to observe any overall relationships between the variables.&lt;/p&gt;
&lt;p&gt;Let&apos;s continue exploring more of what Lets-Plot has in store for us! 📊🐧🌈&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(ggplot(df,
        aes(x=&apos;flipper_length_mm&apos;,
            y = &apos;body_mass_g&apos;,
            color=&apos;species&apos;
           )
       )
 + geom_point() # Draw points defined by an x and y coordinate, as for a scatter plot.
 + geom_smooth(method=&apos;lm&apos;) # Add a smoothed conditional mean. ‘lm’ stands for &apos;linear model&apos; as Smoothing method
) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the previous example, we highlighted the importance of placing the &amp;lt;code&amp;gt;color&amp;lt;/code&amp;gt; parameter at the &lt;em&gt;global level&lt;/em&gt;, which grouped the data by the three penguin species and showed separate regression lines for each group. However, if we prefer to depict the regression line for the entire dataset, regardless of the group association, we can do so just as easily. All we need to do is remove the &amp;lt;code&amp;gt;color&amp;lt;/code&amp;gt; parameter from the aesthetics of the &amp;lt;code&amp;gt;ggplot&amp;lt;/code&amp;gt; function and place it solely in the &amp;lt;code&amp;gt;geom_point&amp;lt;/code&amp;gt;.&lt;/p&gt;
&lt;p&gt;Additionally, to enhance the plot further, we can properly label the x and y axes, add a title and subtitle. With these simple adjustments, we can achieve the same output as &lt;a href=&quot;https://r4ds.hadley.nz/data-visualize#sec-adding-aesthetics-layers&quot;&gt;Hadley&apos;s original code&lt;/a&gt;, with little to no modification.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(ggplot(df, 
        aes(x=&apos;flipper_length_mm&apos;,
            y = &apos;body_mass_g&apos;,
           )
       )
 + geom_point(aes(color=&apos;species&apos;, shape=&apos;species&apos;))
 + geom_smooth(method=&apos;lm&apos;)
 + labs(
    title = &quot;Body mass and flipper length&quot;,
    subtitle = &quot;Dimensions for Adelie, Chinstrap, and Gentoo Penguins&quot;,
    x = &quot;Flipper length (mm)&quot;, y = &quot;Body mass (g)&quot;,
  ) 
 + scale_color_viridis() # lets-plots equivalent of the scale_color_colorblind()
) 
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Visualizing Data Based on Categorical Variables&lt;/h1&gt;
&lt;p&gt;Lets-Plot provides numerous options to showcase our data using categorical variables. From bar plots, box plots, and violin plots to pie charts, the possibilities are diverse. You can check out their &lt;a href=&quot;https://lets-plot.org/pages/api.html&quot;&gt;API&lt;/a&gt; for reference. Let&apos;s explore some examples to demonstrate the versatility of Lets-Plot in visualizing categorical data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 
penguin_bar = (ggplot(df,aes(x=&apos;species&apos;))
               + geom_bar()
              )

penguin_box = (ggplot(df,aes(x = &apos;species&apos;, y = &apos;body_mass_g&apos;))
               + geom_boxplot()
              )

penguin_density = (ggplot(df,aes(&apos;body_mass_g&apos;, color=&apos;species&apos;, fill=&apos;species&apos;))
                   + geom_density(alpha=0.5)
                  )

penguin_rel_frequency = (ggplot(df,aes(x = &apos;island&apos;, fill = &apos;species&apos;))
                         + geom_bar(position=&apos;fill&apos;)
                        )
gggrid([penguin_bar, penguin_box, penguin_density, penguin_rel_frequency], ncol=2)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Incorporate Multiple Variables with facet_wrap&lt;/h1&gt;
&lt;p&gt;So far we&apos;ve discovered how easy it is to plot data based on a single categorical variable. However, what if we want to depict relationships involving two or more categorical variables? That&apos;s where &amp;lt;code&amp;gt;facet_wrap&amp;lt;/code&amp;gt; comes in handy. This versatile function bears resemblance to similar functions found in &amp;lt;code&amp;gt;seaborn&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;ggplot2&amp;lt;/code&amp;gt; libraries.&lt;/p&gt;
&lt;p&gt;To unlock the potential of &amp;lt;code&amp;gt;facet_wrap&amp;lt;/code&amp;gt;, we simply need to define aesthetics, which can either be global or local to the mapping function. Then, we can use &amp;lt;code&amp;gt;facet_wrap&amp;lt;/code&amp;gt; with the relevant categorical variable we want to visualize. It&apos;s as simple as that!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(ggplot(df, aes(x = &apos;flipper_length_mm&apos;, y = &apos;body_mass_g&apos;))  
 + geom_point(aes(color = &apos;species&apos;, shape = &apos;species&apos;), size = 4) 
 + facet_wrap(&apos;island&apos;, nrow=1)
 + labs(title = &quot;Body mass and flipper length based on island&quot;,
        subtitle = &quot;Dimensions for Adelie, Chinstrap, and Gentoo Penguins&quot;,
        x = &quot;Flipper length (mm)&quot;, y = &quot;Body mass (g)&quot;,
       )
 + theme(plot_title=element_text(size=20,face=&apos;bold&apos;))
 + ggsize(1000,500)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;div id=&apos;id-section4&apos;/&amp;gt;&lt;/p&gt;
&lt;h1&gt;Reordering Categorical Variables Based On Statistics&lt;/h1&gt;
&lt;p&gt;When visualizing data, a task I frequently encounter is ordering categorical variables in either ascending or descending order, based on statistics like median or mean. In my previous point on &quot;Visualizing Data Based on Categorical Variables,&quot; you noticed that the boxplot displayed categories in an unordered manner. However, consider how we can present them in an ascending order, determined by the median. This not only enhances the aesthetics of the plot but also provides valuable insights into the relationships among the categories.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(ggplot(df,aes(as_discrete(&apos;species&apos;, order=1, order_by=&apos;..middle..&apos;), 
               y = &apos;body_mass_g&apos;))
 + geom_boxplot()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By incorporating the &amp;lt;code&amp;gt;as_discrete&amp;lt;/code&amp;gt; function, specifying the column, the ordering direction (1 for ascending, -1 for descending), and setting the &amp;lt;code&amp;gt;order_by&amp;lt;/code&amp;gt; variable to &lt;em&gt;middle&lt;/em&gt; (representing the median), the plot has become significantly more informative. This simple addition has allowed us to organize the categorical variables in a meaningful manner, improving the visualization&apos;s clarity and aiding in the interpretation of relationships among the categories.&lt;/p&gt;
&lt;h1&gt;Chaining Pandas Code with Lets Plot Visualization&lt;/h1&gt;
&lt;p&gt;One of the best features of the &amp;lt;code&amp;gt;pandas&amp;lt;/code&amp;gt; library is its remarkable customizability. With the help of the &amp;lt;code&amp;gt;pd.pipe&amp;lt;/code&amp;gt; function, we can seamlessly integrate any of our own functions into method chains, as long as they return a &amp;lt;code&amp;gt;DataFrame&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;Series&amp;lt;/code&amp;gt;. This opens up exciting possibilities to fully incorporate &amp;lt;code&amp;gt;Lets-Plot&amp;lt;/code&amp;gt; into our code, just like &amp;lt;code&amp;gt;pandas&amp;lt;/code&amp;gt;&apos; own built-in plotting functionality.&lt;/p&gt;
&lt;p&gt;While &amp;lt;code&amp;gt;Lets-Plot&amp;lt;/code&amp;gt; may be slightly more verbose than &amp;lt;code&amp;gt;pandas&amp;lt;/code&amp;gt; plotting, it offers significantly more flexibility and freedom for customization. Not to mention that some may consider it visually more appealing. With &amp;lt;code&amp;gt;Lets-Plot&amp;lt;/code&amp;gt; integrated into our &amp;lt;code&amp;gt;pandas&amp;lt;/code&amp;gt; code, we can effortlessly create stunning and informative plots, making data analysis an even more enjoyable experience.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(df
 .groupby(&apos;species&apos;)
 [[&apos;body_mass_g&apos;, &apos;flipper_length_mm&apos;]]
 .mean()
 .reset_index()
 .pipe(lambda df: (ggplot(df)
                   + geom_pie(aes(slice=&apos;body_mass_g&apos;, fill=&apos;species&apos;), 
                              stat=&apos;identity&apos;,size=30, hole=0.2, stroke=1.0,
                              labels=layer_labels().line(&apos;@body_mass_g&apos;).format(&apos;@body_mass_g&apos;, &apos;{.0f}&apos;).size(20)
                             )
                   + labs(title = &quot;Body mass based on species&quot;,
                          subtitle = &quot;Representing how Lets-Plot can be used with pd. pipe&quot;,
                          x = &quot;&quot;, y = &quot;&quot;,
                         )
                   + theme(axis=&apos;blank&apos;,
                          plot_title=element_text(size=20,face=&apos;bold&apos;))
                   + ggsize(500,400)
                  )
 )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s a wrap on the Lets-Plot library! There&apos;s so much more to explore and learn about this powerful tool. I hope you found this introduction helpful and consider integrating Lets-Plot into your daily data analysis routine.&lt;/p&gt;
&lt;p&gt;Happy coding 🐍🖥️🔍🚀&lt;/p&gt;
</content:encoded><author>Adam Cseresznye</author></item><item><title>Tidy Tuesday: Hot Ones Episodes</title><link>https://adamcseresznye.github.io/posts/2023-08-08-hotones</link><guid isPermaLink="true">https://adamcseresznye.github.io/posts/2023-08-08-hotones</guid><pubDate>Wed, 09 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;Hot_Ones_by_First_We_Feast_logo.svg.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I&apos;ve decided to get more involved in the Tidy Tuesday movement. I think it&apos;s a really enjoyable way to improve my skills in working with data, like analyzing, organizing, and visualizing it. The cool datasets they provide make it even more interesting. More information on Tidy Tuesday and their past datasets can be found &lt;a href=&quot;https://github.com/rfordatascience/tidytuesday&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This week we have a dataset related to the show Hot Ones. &lt;a href=&quot;https://www.youtube.com/playlist?list=PLAzrgbu8gEMIIK3r4Se1dOZWSZzUSadfZ&quot;&gt;Hot Ones&lt;/a&gt; is a unique web series that combines spicy food challenges with celebrity interviews. Hosted by Sean Evans, guests tackle increasingly hot chicken wings while answering questions, leading to candid and entertaining conversations. The show&apos;s blend of heat and honesty has turned it into a global sensation, offering a fresh take on interviews and captivating audiences worldwide.&lt;/p&gt;
&lt;p&gt;Let&apos;s see what we can learn from the data 🔬🕵️‍♂️.&lt;/p&gt;
&lt;h1&gt;Import data&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt 
from scipy import stats
from lets_plot import *
from lets_plot.mapping import as_discrete
LetsPlot.setup_html()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have three dataframes: &lt;code&gt;sauces&lt;/code&gt;, &lt;code&gt;season&lt;/code&gt; and &lt;code&gt;episodes&lt;/code&gt;. For more information about the data dictionary see the &lt;a href=&quot;https://github.com/rfordatascience/tidytuesday/blob/master/data/2023/2023-08-08/readme.md&quot;&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sauces=pd.read_csv(&apos;https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2023/2023-08-08/sauces.csv&apos;)
season=pd.read_csv(&apos;https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2023/2023-08-08/seasons.csv&apos;)
episodes=pd.read_csv(&apos;https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2023/2023-08-08/episodes.csv&apos;)

sauces.sample(3)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;season.sample(3)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;episodes.sample(3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
After taking a look at all three datasets, I think I will continue working with the &lt;code&gt;sauces&lt;/code&gt; and &lt;code&gt;episodes&lt;/code&gt; ones.
:::&lt;/p&gt;
&lt;h1&gt;Questions&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;My main questions are:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What differences can be observed in the spiciness of sauces as we look across the various seasons?&lt;/li&gt;
&lt;li&gt;Has every individual successfully completed all the episodes?&lt;/li&gt;
&lt;li&gt;What is the average completion rate per season?&lt;/li&gt;
&lt;li&gt;Is there a correlation between Scoville Units and completion rate?&lt;/li&gt;
&lt;li&gt;Are there any returning guests?&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Data Analysis&lt;/h1&gt;
&lt;h2&gt;What differences can be observed in the spiciness of sauces as we look across the various seasons?&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;
(sauces
 .groupby(&apos;season&apos;)
 .agg(AVG_scoville=(&apos;scoville&apos;, &apos;mean&apos;),
      median_scoville=(&apos;scoville&apos;, &apos;median&apos;),
     )
 .reset_index()
 .melt(id_vars=&apos;season&apos;,
       var_name=&apos;statistics&apos;,
      )
 .pipe(lambda df: (ggplot(df, aes(&apos;season&apos;, &apos;value&apos;,fill=&apos;statistics&apos;))
                   + geom_bar(stat=&apos;identity&apos;, show_legend= False)
                   + facet_wrap(&apos;statistics&apos;,nrow=1,scales=&apos;free_y&apos;)
                   + labs(x=&apos;Seasons&apos;,
                          y=&apos;Scoville Units&apos;
                         )
                   + theme(plot_title=element_text(size=20,face=&apos;bold&apos;))
                  + ggsize(1000,600)
                  )
      )
 
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There seems to be a shift during the season 3-5 period as can be seen in @fig-fig1. Both indicators – &lt;em&gt;mean&lt;/em&gt; and &lt;em&gt;median&lt;/em&gt; Scoville Units – show a consistent upward trend over this time frame and later on they stabilize.&lt;/p&gt;
&lt;p&gt;What about the overall spread of the data? 🤔&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(sauces
 .loc[:, [&apos;season&apos;, &apos;scoville&apos;]]
 .pipe(lambda df: (ggplot(df, aes(&apos;season&apos;, &apos;scoville&apos;))
                   + geom_boxplot()
                   + scale_y_log10()
                   + labs(x=&apos;Seasons&apos;,
                          y=&apos;log(Scoville Units)&apos;
                         )
                   + theme(plot_title=element_text(size=20,face=&apos;bold&apos;))
                   + ggsize(1000,600)
                  )
      )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here are some observations: Season 5 exhibits the widest range, featuring sauces with Scoville Units spanning from 450 to 2,000,000. In addition, starting from season 6 onwards, the &lt;em&gt;averages&lt;/em&gt;, &lt;em&gt;medians&lt;/em&gt;, and &lt;em&gt;ranges&lt;/em&gt; of Scoville Units appear to even out.&lt;/p&gt;
&lt;h2&gt;Has every individual successfully completed all the episodes?&lt;/h2&gt;
&lt;p&gt;To answer this question we will use the &lt;code&gt;episodes&lt;/code&gt; dataframe. Keep in mind there are 300 episodes in this dataframe. The &lt;code&gt;finished&lt;/code&gt; column can be useful here. Just by looking for entries where &lt;code&gt;finished == False&lt;/code&gt; we will have our answer.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(episodes
 .query(&quot;finished==False&quot;)
 [[&apos;season&apos;, &apos;guest&apos;, &apos;guest_appearance_number&apos;]]
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Taking a closer look, it seems that around 15 participants didn&apos;t make it through the entire Hot Ones interview challenge.Not bad out of 300 shows. And guess what? Eric André popped up on the show not just once, but twice! Now, the big question: did he conquer the hot seat in at least one of those interviews? Let&apos;s plot the data to make it more visual...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(episodes
 .query(&quot;finished==False&quot;)
 .groupby(&apos;season&apos;)
 .finished
 .count()
 .to_frame()
 .reset_index()
 .pipe(lambda df: (ggplot(df, aes(&apos;season&apos;, &apos;finished&apos;))
                   + geom_bar(stat=&apos;identity&apos;)
                   + labs(x=&apos;Seasons&apos;,
                          y=&apos;Number of incomplete interviews&apos;
                         )
                   + theme(plot_title=element_text(size=20,face=&apos;bold&apos;))
                   + ggsize(600,400)
                  )
      )

)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Interestingly, a majority of these incomplete interviews belong to season 2 (@fig-fig3). This fact is quite surprising, especially when you consider that the maximum Scoville value for that season was only 550,000 – nearly a quarter of the following year&apos;s value, where only one person faced difficulty finishing the challenge.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://thumbs.gfycat.com/DishonestPoorBluebreastedkookaburra-max-1mb.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;What is the completion rate per season?&lt;/h2&gt;
&lt;p&gt;To get to the bottom of this question, let&apos;s start by figuring out how many episodes were in each season. We can grab this info from the season dataset. Just a heads-up, in season 9 they seem to threw in an extra episode. So, keep this in mind! Otherwise, you might end up with percentages that go beyond 100%.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# First we need to find out the total number of episodes per season

episodes_per_season = (season
                       [[&apos;season&apos;, &apos;episodes&apos;, &apos;note&apos;]]
                       .set_index(&apos;season&apos;)
                       # we need to extract the one extra episode in season 9
                       .assign(note=lambda df: df.note
                               .str.extract(r&apos;([0-9.]+)&apos;)
                               .astype(float),
                        # add the two column together
                               episodes=lambda df: df.episodes
                               .add(df.note, fill_value=0)
                              )
                       .drop(columns=&apos;note&apos;)
                       .squeeze()
                      )
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
completion_rate = (episodes
                   .query(&quot;finished==True&quot;)
                   .groupby(&apos;season&apos;)
                   .finished
                   .sum()
                   .div(episodes_per_season)
                   .mul(100)
                   .to_frame().reset_index()
                   .rename(columns={0:&apos;completion_rate&apos;})
                  )
                   
(completion_rate                  
 .pipe(lambda df: (ggplot(df, aes(&apos;season&apos;, &apos;completion_rate&apos;))
                   + geom_line(stat=&apos;identity&apos;)
                   + labs(x=&apos;Seasons&apos;,
                          y=&apos;% successful participants&apos;
                         )
                   + theme(plot_title=element_text(size=20,face=&apos;bold&apos;))
                   + ggsize(600,400)
                  )
      )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Taking a peek at @fig-fig4, it seems like the normalized completion rate hits its lowest point in season 1, closely followed by season 7. However, even in those seasons, the rate remains surprisingly high.&lt;/p&gt;
&lt;h2&gt;Is there a correlation between Scoville Units and completion rate?&lt;/h2&gt;
&lt;p&gt;Here&apos;s a curious thought: could there be a link between Scoville Units and the completion rate? I&apos;m just wondering if the spiciness level affects how well participants handle the challenge. Exploring this connection might add a spicy twist to the Hot Ones experience – let&apos;s see where the data takes us!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# AVG_scoville code comes from a code snippet 
# &apos;What differences can be observed in the spiciness 
# of sauces as we look across the various seasons?&apos;

AVG_scoville = (sauces
                .groupby(&apos;season&apos;)
                .agg(AVG_scoville=(&apos;scoville&apos;, &apos;mean&apos;))
                .squeeze()
               )

# Let&apos;s calculate the Pearson correlation coefficient. We have to discard the last value
# as the completion rate is not defined for that

stats.pearsonr(AVG_scoville.values[:-1],completion_rate.completion_rate.values[:-1])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Pearson correlation coefficient has its say: there&apos;s actually a moderate positive correlation(0.5, &lt;em&gt;p&lt;/em&gt;&amp;lt;0.05) between Scoville Units and completion rate. Quite intriguing, isn&apos;t it? Honestly, I was expecting the opposite outcome myself! It seems like the higher the average spiciness, the more determined the guests become. Take a look at @fig-fig5.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(pd.concat([AVG_scoville,completion_rate],axis=1)
 .rename(columns={0:&apos;success_rate&apos;})
 .pipe(lambda df: (ggplot(df, aes(&apos;AVG_scoville&apos;, &apos;completion_rate&apos;))
                   + geom_point(size=5, alpha=0.5)
                   + geom_smooth()
                   + labs(x=&apos;Average Scoville units&apos;,
                          y=&apos;% successful participants&apos;
                         )
                   + theme(plot_title=element_text(size=20,face=&apos;bold&apos;))
                   + ggsize(600,400)
                  )
      )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Are there any returning guests?&lt;/h2&gt;
&lt;p&gt;Has there been a brave soul who dared to make a return to the show for a second time? The column &lt;code&gt;guest_appearance_number&lt;/code&gt; holds the answers you&apos;re looking for.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(episodes
 .query(&quot;guest_appearance_number &amp;gt; 1&quot;)
 [[&apos;guest&apos;,&apos;season&apos;, &apos;episode_season&apos;, &apos;finished&apos;]]
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It looks like a total of 12 individuals have taken on the challenge not once, but twice.  Hats off to their courage! 🎩&lt;/p&gt;
&lt;h1&gt;Final words&lt;/h1&gt;
&lt;p&gt;And there you have it, folks – that&apos;s a wrap! I hope you enjoyed exploring the Hot Ones dataset with me. Rest assured, more of these analyses are in the pipeline for the future. Stay tuned for what&apos;s to come!&lt;/p&gt;
</content:encoded><author>Adam Cseresznye</author></item><item><title>🥳🎉 Two IBM certificates and some geospatial data</title><link>https://adamcseresznye.github.io/posts/folium</link><guid isPermaLink="true">https://adamcseresznye.github.io/posts/folium</guid><pubDate>Thu, 10 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://gisgeography.com/wp-content/uploads/2022/05/IBM-Data-Science-Professional-Certificate.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I&apos;m happy to share that I&apos;ve recently completed both IBM&apos;s &lt;a href=&quot;https://www.coursera.org/professional-certificates/ibm-data-analyst&quot;&gt;Data Analyst&lt;/a&gt; and &lt;a href=&quot;https://www.coursera.org/professional-certificates/ibm-data-science&quot;&gt;Data Science Professional Certificates&lt;/a&gt; within the past month. The course content was well-structured, and I learned a great deal from these programs. For instance, I&apos;ve always been interested in learning SQL, and this was the perfect chance to start exploring it.&lt;/p&gt;
&lt;p&gt;If you&apos;re curious about these certificates, you can find more information through the links provided below. But my learning journey doesn&apos;t stop here—I&apos;m planning to tackle most of the courses listed in the &lt;a href=&quot;https://www.coursera.org/learning-paths/data-science&quot;&gt;Data Science learning path&lt;/a&gt; on Coursera, so there&apos;s more to come.&lt;/p&gt;
&lt;p&gt;While I&apos;m at it, I wanted to introduce you to a neat library called &lt;a href=&quot;https://python-visualization.github.io/folium/&quot;&gt;Folium&lt;/a&gt;, which is fantastic for working with geospatial data. I came across Folium during the capstone project of the Data Science Specialization, where we had a fun task of predicting and visualizing the success of SpaceX rocket launches.&lt;/p&gt;
&lt;p&gt;In this post, I&apos;ll briefly share what I&apos;ve learned about this library. I hope you&apos;ll find it useful too. Let&apos;s dive in!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import folium
import pandas as pd
import os
from folium import plugins
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&apos;ll be utilizing the dataset made available by https://open.toronto.ca/. &lt;a href=&quot;https://open.toronto.ca/dataset/street-furniture-bicycle-parking/&quot;&gt;This dataset&lt;/a&gt; includes the locations of bicycles installed on sidewalks and boulevards across the City of Toronto, wherever there&apos;s a requirement for public bicycle parking facilities. By the way, I discovered this dataset through the &lt;a href=&quot;https://github.com/awesomedata/awesome-public-datasets&quot;&gt;&lt;em&gt;Awesome Public Datasets&lt;/em&gt;&lt;/a&gt; repository on GitHub. If you haven&apos;t already, I recommend checking them out.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Let&apos;s read in the file

for file in os.listdir():
    if file.endswith(&quot;.csv&quot;):
        toronto_df = pd.read_csv(file)

        print(f&quot;{file} read in as pandas dataframe&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Considering the original dataset has over 17,300 entries, we&apos;ll keep things light by working with just 500 rows for now. It&apos;s all for the sake of a demonstration, after all!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
toronto_df = toronto_df.sample(n=500)
toronto_df.head()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;em&gt;geometry&lt;/em&gt; column holds the longitude and latitude information, but before we dive in, we need to extract the valuable details. No worries – we&apos;ll make use of pandas&apos; &lt;code&gt;str.extract&lt;/code&gt; for this task.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pattern = r&quot;(-?\d+\.\d+),\s*(-?\d+\.\d+)&quot;

toronto_df_processed = toronto_df.assign(
    LONGITUDE=lambda df: df.geometry.str.extract(pattern)[0],
    LATITUDE=lambda df: df.geometry.str.extract(pattern)[1],
).loc[:, [&quot;ASSETTYPE&quot;, &quot;STATUS&quot;, &quot;LONGITUDE&quot;, &quot;LATITUDE&quot;]]
toronto_df_processed.head()
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Creating the map and displaying it&lt;/h1&gt;
&lt;p&gt;Here&apos;s an example of how to create a map without any overlaid data points.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
toronto_map = folium.Map(
    location=[43.651070, -79.347015], zoom_start=11, tiles=&quot;OpenStreetMap&quot;
)
toronto_map
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Superimposing bike locations on the map with &lt;code&gt;FeatureGroup&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;After instantiating &lt;code&gt;FeatureGroup&lt;/code&gt;, we can easily add the bike locations using the &lt;code&gt;add_child&lt;/code&gt; method. It is really easy!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
# let&apos;s start with a clean copy of the map of Toronto
toronto_map = folium.Map(
    location=[43.651070, -79.347015], zoom_start=11, tiles=&quot;OpenStreetMap&quot;
)

# instantiate a feature group 
bike_stations = folium.map.FeatureGroup()

# loop through the bike stations
for lat, long in zip(toronto_df_processed.LATITUDE, toronto_df_processed.LONGITUDE):
    bike_stations.add_child(
        folium.features.CircleMarker(
            [lat, long],
            radius=5,
            color=&quot;red&quot;,
            fill=True,
            fill_color=&quot;yellow&quot;,
            fill_opacity=1,
        )
    )
# add bike stations to the map
toronto_map.add_child(bike_stations)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Adding pop-up text with relevant information&lt;/h1&gt;
&lt;p&gt;We can also enhance this by adding a pop-up box that displays custom text of our choice.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
# let&apos;s start with a clean copy of the map of Toronto
toronto_map = folium.Map(
    location=[43.651070, -79.347015], zoom_start=11, tiles=&quot;OpenStreetMap&quot;
)

# instantiate a feature group 
bike_stations = folium.map.FeatureGroup()

# loop through the bike stations
for lat, long in zip(toronto_df_processed.LATITUDE, toronto_df_processed.LONGITUDE):
    bike_stations.add_child(
        folium.features.CircleMarker(
            [lat, long],
            radius=5,
            color=&quot;grey&quot;,
            fill=True,
            fill_color=&quot;white&quot;,
            fill_opacity=1,
        )
    )

# add pop-up text to each marker on the map
latitudes = list(toronto_df_processed.LATITUDE)
longitudes = list(toronto_df_processed.LONGITUDE)
labels = list(toronto_df_processed.STATUS)

for lat, lng, label in zip(latitudes, longitudes, labels):
    folium.Marker([lat, lng], popup=label).add_to(toronto_map)

# add bike stations to map
toronto_map.add_child(bike_stations)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Clustering the rental locations with &lt;code&gt;MarkerCluster&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;And the best part, which happens to be my favorite, is that we can also integrate a &lt;code&gt;MarkerCluster&lt;/code&gt;. This comes in handy when we&apos;re dealing with numerous data points clustered closely together on the map. With a &lt;code&gt;MarkerCluster&lt;/code&gt;, you get to see their combined values instead of each one individually. It&apos;s a fantastic feature!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
# let&apos;s start with a clean copy of the map of Toronto
toronto_map = folium.Map(
    location=[43.651070, -79.347015], zoom_start=11, tiles=&quot;OpenStreetMap&quot;
)

# instantiate a mark cluster object 
bike_stations_cluster = plugins.MarkerCluster().add_to(toronto_map)

# loop through the dataframe and add each data point to the mark cluster
for lat, lng, label, in zip(
    toronto_df_processed.LATITUDE,
    toronto_df_processed.LONGITUDE,
    toronto_df_processed.STATUS,
):
    folium.Marker(
        location=[lat, lng],
        icon=None,
        popup=label,
    ).add_to(bike_stations_cluster)

# display map
toronto_map
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s a wrap! I hope these examples have been helpful. Feel free to use these techniques in your next data science or geospatial project. Until next time, happy exploring!&lt;/p&gt;
</content:encoded><author>Adam Cseresznye</author></item><item><title>Getting Started with Polars</title><link>https://adamcseresznye.github.io/posts/polars</link><guid isPermaLink="true">https://adamcseresznye.github.io/posts/polars</guid><pubDate>Fri, 11 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/pola-rs/polars-static/master/web/splash.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In the ever-evolving field of data science, effective data manipulation tools are essential. Enter Polars, a Rust-based library garnering attention within the data community. Boasting impressive speed and versatile capabilities, Polars is redefining our data management practices. In this blog, we delve into Polars&apos; core functions and practical applications, shedding light on how it empowers data professionals to efficiently tackle complex tasks.&lt;/p&gt;
&lt;p&gt;For those well-versed in Pandas, Polars offers a blend of familiarity and innovation. Although this document is not designed to substitute the official documentation, it serves as a tool to provide you with insights into the capabilities that polars offers. Our goal is to ensure the continuous updating of this document.&lt;/p&gt;
&lt;p&gt;Our exploration of Polars is guided by insights from the &lt;a href=&quot;https://pola-rs.github.io/polars-book/user-guide/&quot;&gt;Polars User Guide&lt;/a&gt;, Kevin Heavey&apos;s perspectives in &lt;a href=&quot;https://kevinheavey.github.io/modern-polars/&quot;&gt;Modern Polars&lt;/a&gt; and Matt Harrison&apos;s &lt;a href=&quot;https://www.youtube.com/watch?v=CJ0f45evuME&quot;&gt;engaging tutorial&lt;/a&gt; on Polars at PyCon.
We kickstart our exploration with Matt Harrison&apos;s shared dataset, the US Department of Energy&apos;s &lt;a href=&quot;https://github.com/mattharrison/datasets/blob/master/data/vehicles.csv.zip&quot;&gt;Automobile Fuel Economy data&lt;/a&gt;. Let&apos;s begin this journey!&lt;/p&gt;
&lt;h1&gt;Setup&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;import pandas as pd
import polars as pl
import polars.selectors as cs

import seaborn as sns
import matplotlib.pyplot as plt

pl.Config.set_tbl_rows(2)  # limit the numbers of rows printed
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Polars Version&lt;/h1&gt;
&lt;p&gt;We are using Polars 0.18.8 for this demonstration.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pl.__version__
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Given the large size of the dataset and the fact that some columns aren&apos;t really useful for us, we&apos;ll start by picking out the information we actually need. Feel free to use the code below to work with your dataframe.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df = pl.read_csv(&quot;vehicles.csv&quot;, null_values=&quot;NA&quot;)

# process and save parquet for later use
(
    df.select(
        pl.col(
            &quot;make&quot;,
            &quot;model&quot;,
            &quot;drive&quot;,
            &quot;cylinders&quot;,
            &quot;displ&quot;,
            &quot;fuelType&quot;,
            &quot;mpgData&quot;,
            &quot;city08&quot;,
            &quot;highway08&quot;,
            &quot;comb08&quot;,
            &quot;year&quot;,
        )
    )
    .with_columns(
        pl.col(&quot;make&quot;).cast(pl.Categorical, strict=False),
        pl.col(&quot;model&quot;).cast(pl.Categorical, strict=False),
        pl.col(&quot;drive&quot;).cast(pl.Categorical, strict=False),
        pl.col(&quot;fuelType&quot;).cast(pl.Categorical, strict=False),
        pl.col(&quot;cylinders&quot;).cast(pl.Int8, strict=False),
        pl.col(&quot;displ&quot;).cast(pl.Float32, strict=False),
        pl.col(&quot;mpgData&quot;).map_dict({&quot;N&quot;: False, &quot;Y&quot;: True}),
        pl.col(&quot;city08&quot;).cast(pl.Int8, strict=False),
        pl.col(&quot;highway08&quot;).cast(pl.Int8, strict=False),
        pl.col(&quot;comb08&quot;).cast(pl.Int8, strict=False),
        pl.col(&quot;year&quot;).cast(pl.Utf8).str.to_datetime(&quot;%Y&quot;).cast(pl.Date),
    )
    .write_parquet(&quot;vehicles.parquet&quot;)
)
df = pl.read_parquet(&quot;vehicles.parquet&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Inspect the polars namespace&lt;/h1&gt;
&lt;p&gt;Taking a closer look at the Polars namespace can be really helpful as you start to get to know the library. By exploring the functions, methods, and attributes available within the namespace, you&apos;ll get a better sense of what the library can do. This exploration sets the groundwork for you to use the library more effectively and make the most of its features.&lt;/p&gt;
&lt;p&gt;By issuing &lt;code&gt;print(dir(pl))&lt;/code&gt;, we can explore the accessible functions within the namespace.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# available functions

print(dir(pl))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same applies to the methods available for DataFrames.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# methods defined on dataframes

print(dir(df))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...and expressions for the Series&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# available expressions

print(dir(pl.col(&quot;foo&quot;)))
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Access documentation&lt;/h1&gt;
&lt;p&gt;If you&apos;re familiar with Python, you might recognize that you can access documentation by either using &lt;code&gt;__doc__&lt;/code&gt; or by calling the &lt;code&gt;help()&lt;/code&gt; function.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(df[&quot;year&quot;].dt.convert_time_zone.__doc__)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;help(pl.head)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Indexing&lt;/h1&gt;
&lt;p&gt;If you&apos;ve spent some time working with Pandas, you might find it interesting that Polars doesn&apos;t rely on the concept of an index like Pandas does. But no need to fret—there are several other ways we can still access the rows in Polars. One of the straightforward methods—although I won&apos;t delve too deeply into it here—is using the &lt;code&gt;head&lt;/code&gt;, &lt;code&gt;tail&lt;/code&gt;, and &lt;code&gt;sample&lt;/code&gt; methods.&lt;/p&gt;
&lt;h2&gt;Use &lt;code&gt;take&lt;/code&gt; for Series or Expressions&lt;/h2&gt;
&lt;p&gt;An interesting approach is through Polars&apos; &lt;code&gt;take&lt;/code&gt; method. This method can be used with &lt;code&gt;Expr&lt;/code&gt;, &lt;code&gt;Series&lt;/code&gt;, or &lt;code&gt;np.ndarray&lt;/code&gt;. However, it&apos;s worth noting that if you intend to utilize it with DataFrames, a preliminary conversion to Series is necessary.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
df.select(pl.col(&quot;make&quot;)).to_series().take(list(range(0, 5)))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using &lt;code&gt;filter&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Similar to Pandas, Polars features a &lt;code&gt;filter&lt;/code&gt; method that assists in the process of selectively filtering rows based on one or multiple conditions.&lt;/p&gt;
&lt;h3&gt;Evaluate a single condition&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;df.filter(pl.col(&quot;model&quot;) == &quot;Testarossa&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Combine multiple conditions&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;df.filter((pl.col(&quot;model&quot;) == &quot;Testarossa&quot;) &amp;amp; (pl.col(&quot;make&quot;) == &quot;Ferrari&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Imitate pandas&apos; &lt;code&gt;index&lt;/code&gt; using &lt;code&gt;with_row_count&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Even in the absence of a traditional index, you can make use of the &lt;code&gt;with_row_count&lt;/code&gt; method in Polars. This clever method comes to the rescue for tasks like indexing and filtering, providing an alternative approach.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(df.with_row_count().filter(pl.col(&quot;row_nr&quot;) == 5))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Use &lt;code&gt;is_in&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;is_in&lt;/code&gt; function can be employed to verify whether elements of a given expression are present within another Series. We can use this to filter our rows.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.filter(pl.col(&quot;cylinders&quot;).is_in([i for i in range(6, 8)]))
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Select columns&lt;/h1&gt;
&lt;p&gt;Before we dive into the realm of selecting columns, it&apos;s important to introduce the concepts of &lt;code&gt;with_columns&lt;/code&gt; and &lt;code&gt;select&lt;/code&gt;, while also highlighting the distinctions between these two methods.&lt;/p&gt;
&lt;h2&gt;Difference between &lt;code&gt;select&lt;/code&gt; and  with_columns`&lt;/h2&gt;
&lt;p&gt;As evident from the examples below, both &lt;code&gt;select&lt;/code&gt; and &lt;code&gt;with_column&lt;/code&gt; can be utilized for both column selection and column assignment. However, there&apos;s a crucial distinction between the two. After performing column operations, the &lt;code&gt;select&lt;/code&gt; method drops the unselected columns and retains only the columns that underwent operations. Conversely, the &lt;code&gt;with_column&lt;/code&gt; method appends the new columns to the dataframe, preserving the original ones.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
# everything else is dropped

(
    df.select(
        (pl.col(&quot;city08&quot;) + 10).alias(&quot;added&quot;),
        (pl.col(&quot;city08&quot;) - 10).alias(&quot;substracted&quot;),
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# columns are kept

(
    df.with_columns(
        (pl.col(&quot;city08&quot;) + 10).alias(&quot;added&quot;),
        (pl.col(&quot;city08&quot;) - 10).alias(&quot;substracted&quot;),
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we&apos;ve clarified this point, let&apos;s proceed to explore the fundamental methods for selecting columns.&lt;/p&gt;
&lt;h2&gt;Select all columns&lt;/h2&gt;
&lt;p&gt;A particularly handy tool is &lt;code&gt;pl.all()&lt;/code&gt;, which provides the current state of the dataframe—similar to &lt;code&gt;pd.assign(foo=lambda df)&lt;/code&gt; in Pandas. This can prove useful, particularly when dealing with operations like groupby and aggregation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# this is analogous to df.select(pl.col(&quot;*&quot;)), where * represents the wildcard component
df.select(pl.all())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Select all columns except for...&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;df.select(pl.col(&quot;*&quot;).exclude(&quot;year&quot;, &quot;comb08&quot;, &quot;highway08&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Select a subset of columns&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;
df.select(pl.col(&quot;year&quot;, &quot;comb08&quot;, &quot;highway08&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Select columns based on regular expression&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;
df.select(pl.col(&quot;^.*(mo|ma).*$&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Select columns based on dtypes&lt;/h2&gt;
&lt;p&gt;This may remind you of pandas&apos; &lt;code&gt;select_dtypes&lt;/code&gt; method.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.select(pl.col(pl.Int8, pl.Boolean))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using &lt;code&gt;selectors&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Selectors offer a more intuitive approach to selecting columns from DataFrame or LazyFrame objects, taking into account factors like column names, data types, and other properties. They consolidate and enhance the functionality that&apos;s accessible through the &lt;code&gt;col()&lt;/code&gt; expression. More on them &lt;a href=&quot;https://pola-rs.github.io/polars/py-polars/html/reference/selectors.html#selectors&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;By dtypes&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;df.select(cs.integer(), cs.float(), cs.temporal())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# all columns except for the ones containing float
df.select(cs.all() - cs.numeric())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# same as the one above but it uses the tilde
df.select(~cs.numeric())
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;By column names&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;
df.select(cs.contains(&quot;08&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
df.select(cs.starts_with(&quot;d&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The possibilities here are incredibly vast! I&apos;m pretty sure you&apos;ll find a function that suits your needs just right.&lt;/p&gt;
&lt;h1&gt;&lt;code&gt;dtype&lt;/code&gt; conversions&lt;/h1&gt;
&lt;p&gt;Understanding and managing data types, as well as converting them, are very important—particularly when dealing with larger datasets. Luckily, with Polars, this process is wonderfully straightforward. Here&apos;s a useful tip: in many cases, the &lt;code&gt;cast&lt;/code&gt; method will be your trusty companion for data type conversions. It&apos;s quite versatile and often does the job seamlessly. When it comes to converting dates, things can get a bit more intricate, but rest assured, it&apos;s a hurdle you can easily overcome!&lt;/p&gt;
&lt;p&gt;More info on the available dtypes can be found &lt;a href=&quot;https://pola-rs.github.io/polars-book/user-guide/concepts/data-types/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    df.select(
        pl.col(&quot;cylinders&quot;).cast(pl.Int32),
        pl.col(&quot;displ&quot;).cast(pl.Int64),
        pl.col(&quot;mpgData&quot;).cast(pl.Int64),
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Working with &lt;code&gt;str&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;We can examine the available attributes when working with strings. You might notice that it boasts some familiar attributes, like &lt;code&gt;extract&lt;/code&gt;, &lt;code&gt;replace&lt;/code&gt;, &lt;code&gt;split&lt;/code&gt;, and &lt;code&gt;strip&lt;/code&gt;, among others.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# attributes of str
print(dir(pl.col(&quot;*&quot;).str))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Basic operations&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;(
    df.select(
        pl.col(&quot;make&quot;).cast(pl.Utf8).str.contains(&quot;Hyundai|Kia&quot;).alias(&quot;contains&quot;),
        pl.col(&quot;make&quot;).cast(pl.Utf8).str.starts_with(&quot;Alfa&quot;).alias(&quot;starts_with&quot;),
        pl.col(&quot;make&quot;).cast(pl.Utf8).str.ends_with(&quot;ari&quot;).alias(&quot;ends_with&quot;),
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can extract the numbers from the different car models:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    df.select(
        pl.col(&quot;model&quot;)
        .cast(pl.Utf8)
        .str.extract(r&quot;(\d+)&quot;, group_index=1)
        .cast(pl.Int32)
        .alias(&quot;extracted_number&quot;),
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As per usual, we can replace values in a given column:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    df.select(
        pl.col(&quot;make&quot;).cast(pl.Utf8).str.replace(&quot;r&quot;, 0).alias(&quot;replaced&quot;),
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Changing column names&lt;/h2&gt;
&lt;p&gt;Altering column names is quite reminiscent of the process in Pandas:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.rename({&quot;make&quot;: &quot;car maker&quot;, &quot;model&quot;: &quot;car model&quot;})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In case you would like to alter multiple column names all at once:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.select(pl.all().map_alias(lambda name: name.upper().replace(&quot;I&quot;, &quot;0000&quot;)))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;str.split&lt;/code&gt; with &lt;code&gt;expand=&apos;all&apos;&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;I often find myself using the &lt;code&gt;str.split&lt;/code&gt; method, and it caught me off guard that there isn&apos;t a direct equivalent in Polars. Fingers crossed, we might come across an implementation like &lt;code&gt;expand=True&lt;/code&gt; from Pandas, which would be a real game-changer here too!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    df.select(
        pl.col(&quot;drive&quot;)
        .cast(pl.Utf8)
        .str.split_exact(&quot;-&quot;, n=1)
        .struct.rename_fields([&quot;first_part&quot;, &quot;second_part&quot;])
        .alias(&quot;fields&quot;),
    ).unnest(&quot;fields&quot;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Aggregation&lt;/h1&gt;
&lt;p&gt;Split-apply-combine operations are where Polars truly excels. This is largely due to its foundation in Rust, Polars&apos; use of lazy evaluation, parallel execution, and efficient memory management also contribute to its speed. As a result, split-apply-combine operations are significantly faster in Polars compared to conventional methods.&lt;/p&gt;
&lt;h2&gt;A simple split-apply-combine operation&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;
(df.groupby(&quot;make&quot;).count().sort(by=&quot;count&quot;, descending=True).limit(5))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using &lt;code&gt;agg&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;You can use &lt;code&gt;agg&lt;/code&gt; to calculate statistics over either a single or multiple columns all at once.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(df.groupby(&quot;make&quot;).agg(pl.count(), pl.col(&quot;model&quot;)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&apos;s an illustration of how to utilize the &lt;code&gt;is_not_nan&lt;/code&gt; method. In Polars, unknown values are represented with floating-point precision, similar to how pandas handles them. It&apos;s important not to mix this up with missing values, which are indicated by &lt;code&gt;Null&lt;/code&gt; in Polars. Additionally, take note of the use of &lt;code&gt;limit&lt;/code&gt; instead of &lt;code&gt;head&lt;/code&gt;, a concept quite akin to SQL.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(
    df.groupby(&quot;make&quot;)
    .agg(
        pl.col(&quot;city08&quot;).mean().alias(&quot;mean_cyl&quot;),
        pl.col(&quot;displ&quot;).mean().alias(&quot;mean_disp&quot;),
    )
    .filter(pl.col(&quot;mean_cyl&quot;).is_not_nan())
    .sort(by=[&quot;mean_cyl&quot;, &quot;mean_disp&quot;], descending=[True, True])
    .limit(5)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Aggregated columns can be renamed immediately with the &lt;code&gt;alias&lt;/code&gt; method.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(
    df.groupby(&quot;make&quot;, &quot;fuelType&quot;)
    .agg(pl.col(&quot;comb08&quot;).mean().alias(&quot;filtered&quot;))
    .filter((pl.col(&quot;fuelType&quot;) == &quot;CNG&quot;) | (pl.col(&quot;fuelType&quot;) == &quot;Diesel&quot;))
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&apos;s a scenario for performing aggregation based on the year. In this case, the &quot;year&quot; column holds the year-related data, which can be extracted using the &lt;code&gt;dt.year&lt;/code&gt; attribute. Keep in mind that Polars doesn&apos;t have native plotting capabilities like pandas. To visualize the data, as a final step, you can convert the dataframe to pandas and make use of its &lt;code&gt;.plot&lt;/code&gt; method.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(
    df.groupby(
        [
            pl.col(&quot;year&quot;).dt.year().alias(&quot;year&quot;),
        ]
    )
    .agg([pl.col(&quot;comb08&quot;).mean(), pl.col(&quot;city08&quot;).mean()])
    .sort(
        [
            &quot;year&quot;,
        ]
    )
    .to_pandas()
    .set_index(&quot;year&quot;)
    .plot()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&apos;s another fantastic option for you: the &lt;code&gt;groupby_dynamic&lt;/code&gt; feature. It&apos;s really impressive, by the way. You can employ this when grouping based on years or any other time-related information. Additionally, you can make use of the &lt;code&gt;every&lt;/code&gt; parameter for resampling, which closely resembles what you can do with pandas.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(
    df.sort(by=&quot;year&quot;)
    .groupby_dynamic(index_column=&quot;year&quot;, every=&quot;2y&quot;)
    .agg([pl.col(&quot;comb08&quot;).mean(), pl.col(&quot;city08&quot;).mean()])
    .sort(
        [
            &quot;year&quot;,
        ]
    )
    .to_pandas()
    .set_index(&quot;year&quot;)
    .plot()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pivot&lt;/h2&gt;
&lt;p&gt;You can also accomplish split-apply-combine tasks seamlessly using the pivot function. It&apos;s remarkably straightforward.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(
    df.with_columns(
        pl.col(&quot;fuelType&quot;).cast(pl.Utf8)
    )  # conversion to str is needed for the next step
    .filter(pl.col(&quot;fuelType&quot;).is_in([&quot;Regular&quot;, &quot;Premium&quot;]))
    .pivot(values=&quot;city08&quot;, index=&quot;make&quot;, columns=&quot;fuelType&quot;, aggregate_function=&quot;mean&quot;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Handling missing data&lt;/h1&gt;
&lt;h2&gt;Getting missing value count per column&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;null_count&lt;/code&gt; function showcases the count of missing values in the dataframe. Remember, these are not the same as np.nans; it&apos;s important to be aware of this distinction. This functionality is akin to the pandas &lt;code&gt;df.isna().sum()&lt;/code&gt; operation if you&apos;re familiar with pandas.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.null_count()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Getting boolean array of missing values&lt;/h2&gt;
&lt;p&gt;If you ever need to manipulate a series based on its null values, you can also obtain a boolean mask using the &lt;code&gt;is_null&lt;/code&gt; function. This can be really handy for targeted data manipulation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
df.select(pl.col(&quot;city08&quot;).is_null())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Filling missing values&lt;/h2&gt;
&lt;p&gt;You have a variety of options at your disposal for filling in missing values. Here&apos;s a list of some of the most common ones, although it&apos;s not an exhaustive rundown.&lt;/p&gt;
&lt;h3&gt;With literals&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;(df.with_columns(pl.col(&quot;cylinders&quot;).fill_null(pl.lit(2))))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;(df.with_columns(pl.col(&quot;cylinders&quot;).fill_null(strategy=&quot;zero&quot;)))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;(df.with_columns(pl.col(&quot;cylinders&quot;).fill_null(strategy=&quot;forward&quot;)))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;With an expression&lt;/h3&gt;
&lt;p&gt;This can be very useful for machine learning pipelines. When addressing missing values, you can set them to the mode, median, mean, or any other value that suits your needs.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(df.with_columns(pl.col(&quot;cylinders&quot;).fill_null(pl.col(&quot;cylinders&quot;).mode())))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Using the &lt;code&gt;over&lt;/code&gt; function&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;over&lt;/code&gt; function in Polars is used to perform window operations, akin to what you&apos;d achieve with window functions in SQL or the &lt;code&gt;transform&lt;/code&gt; function in pandas. This function enables you to compute aggregations over a specified window or range of rows defined by a window specification. It&apos;s perfect for tasks like calculating rolling averages, cumulative sums, and other operations that involve working with a specific subset of rows within the dataset.&lt;/p&gt;
&lt;p&gt;Let&apos;s begin by filtering the car makes to encompass only the top 5 car brands. Following that, we can utilize the &lt;code&gt;over&lt;/code&gt; function to derive several statistics.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;top5 = (
    df.select(pl.col(&quot;make&quot;))
    .to_series()
    .value_counts()
    .sort(by=&quot;counts&quot;, descending=True)
    .limit(3)
    .select(pl.col(&quot;make&quot;))
    .to_series()
)

(
    df.filter(pl.col(&quot;make&quot;).is_in(top5)).select(
        &quot;make&quot;,
        &quot;model&quot;,
        pl.col(&quot;city08&quot;).mean().over(&quot;make&quot;).alias(&quot;avg_city_mpg_by_make&quot;),
        pl.col(&quot;city08&quot;).mean().over(&quot;model&quot;).alias(&quot;avg_city_mpg_by_model&quot;),
        pl.col(&quot;comb08&quot;).mean().over([&quot;make&quot;, &quot;model&quot;]).alias(&quot;avg_comb_mpg_by_model&quot;),
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Other common operations&lt;/h1&gt;
&lt;p&gt;In the upcoming section, you&apos;ll discover a mix of handy operations that I&apos;ve ended up using quite often. These don&apos;t exactly fit into the categories we&apos;ve covered earlier, but they&apos;re still quite useful!&lt;/p&gt;
&lt;h2&gt;Operating on multiple columns and renaming them&lt;/h2&gt;
&lt;p&gt;You have the flexibility to conduct column operations and then easily rename them. Additionally, you can make use of the &lt;code&gt;prefix&lt;/code&gt; and &lt;code&gt;suffix&lt;/code&gt; functions to streamline your workflow.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(df.with_columns((cs.numeric() * 2).prefix(&quot;changed_&quot;)))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Count unique values&lt;/h2&gt;
&lt;p&gt;Absolutely, you can also count the unique values within a series in Polars, much like you would with the &lt;code&gt;unique&lt;/code&gt; function in pandas. This can be really useful for understanding the distribution and diversity of data within a specific column.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df.select(pl.col(&quot;make&quot;)).n_unique()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Value_counts to get number of occurrences per item&lt;/h2&gt;
&lt;h3&gt;On a single column&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;
(df.select(pl.col(&quot;make&quot;)).to_series().value_counts(sort=True))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
(df.select(pl.col(&quot;make&quot;)).groupby(&quot;make&quot;).count().sort(by=&quot;count&quot;, descending=True))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;On multiple columns&lt;/h3&gt;
&lt;p&gt;I noticed that Polars lacks the capability to perform value counts on multiple columns, unlike pandas&apos; &lt;code&gt;value_counts&lt;/code&gt; function which operates only on series. However, I&apos;ve discovered that combining a &lt;code&gt;groupby&lt;/code&gt; operation with a &lt;code&gt;count&lt;/code&gt; function can effectively achieve the same outcome for multiple columns. It&apos;s all about finding alternative approaches that get the job done!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    df.select(pl.col(&quot;make&quot;, &quot;model&quot;))
    .groupby([&quot;make&quot;, &quot;model&quot;])
    .count()
    .sort(by=&quot;count&quot;, descending=True)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conditionals/ if-else statements&lt;/h2&gt;
&lt;p&gt;If you&apos;re looking to integrate conditional if-else statements into your Polars chain, you can make use of the &lt;code&gt;when&lt;/code&gt;, &lt;code&gt;then&lt;/code&gt;, and &lt;code&gt;otherwise&lt;/code&gt; functions. However, keep in mind that a series of chained &lt;code&gt;when&lt;/code&gt;, &lt;code&gt;then&lt;/code&gt; statements should be interpreted as if, elif, ... elif, rather than if, if, ... if. In other words, the first condition that evaluates to True will be selected. It&apos;s crucial to understand this distinction when crafting your conditions.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    df.select(
        pl.col(&quot;make&quot;),
        pl.when(pl.col(&quot;comb08&quot;) &amp;lt; 15)
        .then(&quot;bad&quot;)
        .when(pl.col(&quot;comb08&quot;) &amp;lt; 30)
        .then(&quot;good&quot;)
        .otherwise(&quot;very good&quot;)
        .alias(&quot;fuel_economy&quot;),
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alright, folks, that&apos;s it for this introduction to Polars. I know it wasn&apos;t exactly short, but I hope it was informative! I&apos;ll be adding more content to this article in the future, so stay tuned. Until then, happy coding 😎💻🔎&lt;/p&gt;
&lt;h1&gt;Document version&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Comment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.1&lt;/td&gt;
&lt;td&gt;8/11/2023&lt;/td&gt;
&lt;td&gt;Document created&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded><author>Adam Cseresznye</author></item><item><title>The Ultimate Speed Test: Pandas vs Polars </title><link>https://adamcseresznye.github.io/posts/polars-speed</link><guid isPermaLink="true">https://adamcseresznye.github.io/posts/polars-speed</guid><pubDate>Sun, 13 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;hans-jurgen-mager-qQWV91TTBrE-unsplash.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Once I shared my first article about &lt;a href=&quot;https://adamcseresznye.github.io/blog/posts/polars/Polars_revised.html&quot;&gt;&quot;Getting Started with Polars&quot;&lt;/a&gt; I started thinking about something exciting: comparing the speed of Polars and Pandas. This curiosity was sparked by all the buzz around the brand-new &lt;a href=&quot;https://pandas.pydata.org/docs/dev/whatsnew/v2.0.0.html&quot;&gt;Pandas 2.0 release&lt;/a&gt;, promising lightning-fast performance. Pandas 2.0 was announced to come packed with cool features, including the addition of Apache Arrow (pyarrow) as its backing memory format. The big perk of Apache Arrow is that it makes operations speedier and more memory-friendly. Naturally, this got me wondering: how does Pandas 2.0 measure up against Polars? Let&apos;s dive in and find out!&lt;/p&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;p&gt;Keep in mind: occasionally, I refer to some performance difference figures. Even though I repeated these experiments 100 times each, because computations can have a bit of randomness, there might be slight variations in the exact numbers.&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h1&gt;Setup&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;import pandas as pd
import polars as pl
import polars.selectors as cs

import numpy as np
import timeit
import random

import plotly.io as pio
import plotly.express as px

pio.templates.default = &quot;presentation&quot;

print(pd.__version__)
print(pl.__version__)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;Throughout this article, we&apos;ll be working with the latest versions of the packages: Pandas 2.0.3 and Polars 0.18.11.&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h1&gt;Generate the synthetic data&lt;/h1&gt;
&lt;p&gt;For the purpose of benchmarking, we&apos;re going to create our own dataset. To ensure simplicity and uniformity, we&apos;ll generate a dataframe containing columns like name, year of birth, and city of residence – pretty straightforward stuff. We&apos;re aiming for a dataset of 10,000 rows. While we could certainly run this benchmark on a dataframe with more than a million rows, our intention is to provide a realistic example. The code we&apos;ll provide allows you to effortlessly explore various scenarios according to your requirements. Feel free to use it or modify it if you want to perform your own test.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def create_sample_dataframe(n_rows: int, name: str, seed: int = 42) -&amp;gt; pl.DataFrame:
    &quot;&quot;&quot;
    Create a sample Polars DataFrame with the specified number of rows.

    This function generates random data for several columns and creates a Polars
    DataFrame with the specified number of rows. The generated data includes information
    about names, birth years, cities, zip codes, incomes, marital status, number of children,
    and car brands. The resulting DataFrame is written to CSV and Parquet files named
    &apos;sample.csv&apos; and &apos;{name}.parquet&apos;, respectively.

    Parameters
    ----------
    n_rows : int
        The number of rows to generate in the sample DataFrame.
    name : str
        The name to be used when writing the Parquet file.
    seed : int, optional
        The seed to use when generating random data. Default is 42.

    Returns
    -------
    pl.DataFrame
        The first 5 rows of the generated sample DataFrame.
    &quot;&quot;&quot;
    random.seed(seed)
    np.random.seed(seed)

    data = {
        &quot;name&quot;: np.random.choice([&quot;Alice&quot;, &quot;Bob&quot;, &quot;Charlie&quot;, &quot;David&quot;, &quot;Eva&quot;], n_rows),
        &quot;born&quot;: np.random.randint(1950, 1990, size=n_rows),
        &quot;city&quot;: np.random.choice(
            [&quot;Shanghai&quot;, &quot;San Francisco&quot;, &quot;London&quot;, &quot;Munich&quot;, &quot;Mumbai&quot;], n_rows
        ),
        &quot;zip_code&quot;: np.random.randint(10000, 99999, size=n_rows),
        &quot;income&quot;: np.random.normal(50000, 10000, size=n_rows),
        &quot;is_married&quot;: np.random.choice([True, False], n_rows),
        &quot;children&quot;: np.random.randint(0, 5, size=n_rows),
        &quot;car&quot;: np.random.choice([&quot;Ford&quot;, &quot;BMW&quot;, &quot;Toyota&quot;, &quot;Bentley&quot;, &quot;Mini&quot;], n_rows),
    }
    temp_df = pl.DataFrame(data)
    temp_df.write_csv(&quot;sample.csv&quot;)
    temp_df.write_parquet(f&quot;{name}.parquet&quot;)
    return temp_df.head()


create_sample_dataframe(n_rows=10_000, name=&quot;sample&quot;, seed=1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Define helper class&lt;/h1&gt;
&lt;p&gt;In order to streamline our code, we&apos;ll create a convenient helper class featuring two essential methods: &lt;code&gt;get_time&lt;/code&gt; and &lt;code&gt;get_figure&lt;/code&gt;. The &lt;code&gt;get_time&lt;/code&gt; method leverages the &lt;code&gt;timeit.repeat&lt;/code&gt; function to accurately gauge the execution time of our code. In this context, we&apos;ll set the parameters &lt;code&gt;number = 1&lt;/code&gt; and &lt;code&gt;repeat = 100&lt;/code&gt;. This configuration runs the operations 100 times, allowing us to derive key statistics like the mean, median, and standard deviation.&lt;/p&gt;
&lt;p&gt;On the other hand, the &lt;code&gt;get_figure&lt;/code&gt; method takes these gathered results and generates a visually appealing image using Plotly.&lt;/p&gt;
&lt;p&gt;To bring it all together and keep things simple, we&apos;ve designed the &lt;code&gt;run_test&lt;/code&gt; function. This function orchestrates the entire process, seamlessly merging the steps outlined above.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Timeit:
    &quot;&quot;&quot;
    A class for measuring the execution time of multiple functions.
    This class provides methods for measuring the execution time of
    multiple functions using the `timeit` module. The functions to be
    tested are passed to the constructor as a dictionary, where the
    keys are the names of the functions and the values are the functions
    themselves. The `number` and `repeat` parameters control how many times
    each function is executed and how many times the timing is repeated,
    respectively.

    The `get_time` method measures the execution time of each function and
    stores the results in a dictionary, where the keys are the names of
    the functions and the values are lists of execution times. The `get_figure`
    method generates a box plot of the execution times using the `plotly.express.box`
    function.

    Parameters
    ----------
    func_dict : dict
        A dictionary of functions to test, where the keys are the names of the
        functions and the values are the functions themselves.
    number : int, optional
        The number of times to execute each function in each timing run (default is 1).
    repeat : int, optional
        The number of times to repeat each timing run (default is 20).

    Attributes
    ----------
    results : dict or None
        A dictionary of execution times for each function, where the keys are the names
        of the functions and the values are lists of execution times. This attribute is
        `None` until `get_time` is called.

    Methods
    -------
    get_time()
        Measure the execution time of each function and store the results in the `results` attribute.
    get_figure()
        Generate a box plot of the execution times using `plotly.express.box`.
    &quot;&quot;&quot;

    def __init__(self, func_dict, number=1, repeat=100):
        self.func_dict = func_dict
        self.number = number
        self.repeat = repeat
        self.results = None

    def get_time(self):
        &quot;&quot;&quot;
        Measure the execution time of each function and store the results.
        This method uses the `timeit.repeat` function to measure the execution time of each function
        in `func_dict`. The results are stored in a dictionary, where the keys are the names of
        the functions and the values are lists of execution times. This dictionary is also stored
        in the `results` attribute of the `Timeit` object.

        Returns
        -------
        dict
            A dictionary of execution times for each function.
        &quot;&quot;&quot;
        results = {}
        for k, v in self.func_dict.items():
            results[k] = timeit.repeat(v, number=self.number, repeat=self.repeat)
        self.results = results
        return results

    def get_figure(self):
        &quot;&quot;&quot;
        Generate a box plot of the execution times.
        This method uses `plotly.express.box` to generate a box plot of the execution times
        stored in `results`.If `results` is `None`, this method raises a `ValueError`.

        Returns
        -------
        plotly.graph_objs.Figure
            A box plot figure object.

        Raises
        ------
        ValueError
            If no results are available (i.e., if `get_time` has not been called).
        &quot;&quot;&quot;
        if self.results is None:
            raise ValueError(&quot;No results available. Please run get_time first.&quot;)
        df = pd.DataFrame(self.results)
        mean_times = df.median() * 1000
        title = f&apos;Median Execution Times: &amp;lt;br&amp;gt;&amp;lt;sup&amp;gt;{&quot;, &quot;.join([f&quot;{k}={v:.6f}ms&quot; for k,v in mean_times.items()])}&amp;lt;/sup&amp;gt;&apos;
        return px.box(
            df,
            points=&quot;all&quot;,
            labels={&quot;value&quot;: &quot;time (sec)&quot;, &quot;variable&quot;: &quot;&quot;},
            title=title,
            width=600,
            height=500,
        )


def run_test(test: dict, name: str):
    &quot;&quot;&quot;
    Run a timing test on a dictionary of functions using the Timeit class and
    return the results as a DataFrame.

    This function takes a dictionary of functions as input, where the keys are
    the names of the functions and the values are the functions themselves. It creates
    a Timeit object with this dictionary and uses it to measure the execution time of
    each function. The results are then displayed as a box plot using the `get_figure`
    method of the Timeit object and returned as a Pandas DataFrame with columns renamed
    according to the `name` argument.

    Parameters
    ----------
    test : dict
        A dictionary of functions to test, where the keys are the names of the functions
        and the values are the functions themselves.
    name : str
        A string used to rename the columns of the returned DataFrame.

    Returns
    -------
    pd.DataFrame
        A DataFrame containing the results of the timing test, with columns renamed
        according to the `name` argument.
    &quot;&quot;&quot;

    # Create a Timeit object
    t = Timeit(test)
    # Measure the execution time of the functions
    results = t.get_time()
    # Generate a box plot of the execution times
    fig = t.get_figure()
    fig.show()

    return pd.DataFrame(results).rename(
        columns={&quot;Pandas&quot;: f&quot;{name}_pandas&quot;, &quot;Polars&quot;: f&quot;{name}_polars&quot;}
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Reading in data From a CSV File and parquet&lt;/h1&gt;
&lt;h2&gt;CSV&lt;/h2&gt;
&lt;p&gt;Reading CSV files from disk is a task that data scientists often find themselves doing. Now, let&apos;s see how these two libraries compare for this particular job. To maximize the blazing-fast data handling capabilities of PyArrow, we&apos;ll equip Pandas with the &lt;code&gt;engine=&quot;pyarrow&quot;&lt;/code&gt; and &lt;code&gt;dtype_backend=&quot;pyarrow&quot;&lt;/code&gt; arguments. Let&apos;s see how these choices shape the performance!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
test_dict = {
    &quot;Pandas&quot;: lambda: pd.read_csv(
        &quot;sample.csv&quot;, engine=&quot;pyarrow&quot;, dtype_backend=&quot;pyarrow&quot;
    ),
    &quot;Polars&quot;: lambda: pl.read_csv(&quot;sample.csv&quot;),
}
read_csv = run_test(test_dict, &quot;Read csv&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the sake of comparison, we&apos;ll also demonstrate the timeit function invoked using Jupyter cell magic. You&apos;ll notice that the numbers generated this way are quite closely aligned with ours.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%%timeit
pd.read_csv(&quot;sample.csv&quot;, engine=&quot;pyarrow&quot;, dtype_backend=&quot;pyarrow&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;%%timeit
pl.read_csv(&quot;sample.csv&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;parquet&lt;/h2&gt;
&lt;p&gt;Now, let&apos;s read the data in Parquet format.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
test_dict = {
    &quot;Pandas&quot;: lambda: pd.read_parquet(
        &quot;sample.parquet&quot;, engine=&quot;pyarrow&quot;, dtype_backend=&quot;pyarrow&quot;
    ),
    &quot;Polars&quot;: lambda: pl.read_parquet(&quot;sample.parquet&quot;),
}
read_parquet = run_test(test_dict, &quot;Read parquet&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;Polars unquestionably wins this round, it can boast a speed advantage of 2 to 4 times over Pandas.
:::&lt;/p&gt;
&lt;h1&gt;Selecting Columns&lt;/h1&gt;
&lt;p&gt;Alright, let&apos;s spice things up a bit and select some columns to see which library races ahead in terms of speed!&lt;/p&gt;
&lt;h2&gt;Files awaiting in-memory reading&lt;/h2&gt;
&lt;p&gt;A clever approach to conserve memory and enhance speed involves reading only the columns essential for operations. Consider a scenario where we&apos;re interested in displaying just the names from this dataset. The big question now: how do these libraries measure up in terms of speed? Let&apos;s find out!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
test_dict = {
    &quot;Pandas&quot;: lambda: pd.read_csv(
        &quot;sample.csv&quot;, engine=&quot;pyarrow&quot;, dtype_backend=&quot;pyarrow&quot;, usecols=[&quot;name&quot;]
    ),
    &quot;Polars&quot;: lambda: pl.scan_csv(&quot;sample.csv&quot;).select(pl.col(&quot;name&quot;)).collect(),
}
select_col_not_in_memory = run_test(test_dict, &quot;Select column (not in memory)&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;File is in memory&lt;/h2&gt;
&lt;p&gt;As anticipated, Polars continues to showcase its swiftness. It&apos;s worth highlighting the usage of the &lt;code&gt;lazy&lt;/code&gt; and &lt;code&gt;collect&lt;/code&gt; methods in Polars. These nifty tools grant us access to the library&apos;s clever query optimization techniques, which play a pivotal role in significantly enhancing performance. OK, one step further: suppose our files are already loaded into memory. Would there still be a distinction in performance under this circumstance?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df_pandas = pd.read_csv(&quot;sample.csv&quot;, engine=&quot;pyarrow&quot;, dtype_backend=&quot;pyarrow&quot;)
df_polars = pl.read_csv(&quot;sample.csv&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
test_dict = {
    &quot;Pandas&quot;: lambda: df_pandas.loc[:, &quot;name&quot;],
    &quot;Polars&quot;: lambda: df_polars.lazy().select(pl.col(&quot;name&quot;)).collect(),
}
select_col_in_memory = run_test(test_dict, &quot;Select column&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;While Polars showed a significant speed advantage for tasks involving pre-read files, both libraries perform similarly when the files are already in memory.
:::&lt;/p&gt;
&lt;h1&gt;Filtering Rows&lt;/h1&gt;
&lt;p&gt;Now, let&apos;s explore the scenario where we filter our dataset based on one or more column values&lt;/p&gt;
&lt;h2&gt;Based on one condition&lt;/h2&gt;
&lt;p&gt;For our simple scenario, we&apos;ll be narrowing down our focus to filter data based on individuals with the name &quot;David&quot;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
test_dict = {
    &quot;Pandas&quot;: lambda: (df_pandas.query(&quot;name==&apos;David&apos;&quot;)),
    &quot;Polars&quot;: lambda: (df_polars.lazy().filter((pl.col(&quot;name&quot;) == &quot;David&quot;)).collect()),
}
filter_row_one_condition = run_test(test_dict, &quot;Filter (simple)&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Based on multiple conditions&lt;/h2&gt;
&lt;p&gt;Now, for a more intricate challenge, we&apos;re going to dive into querying the data to extract individuals who meet specific criteria: those named David, born after 1980, residing in a city other than London, married, and with three children.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
test_dict = {
    &quot;Pandas&quot;: lambda: (
        df_pandas.query(
            &quot;name==&apos;David&apos; and born&amp;gt;1980 and city != &apos;London&apos; or is_married == True and children &amp;gt;= 3&quot;
        )
    ),
    &quot;Polars&quot;: lambda: (
        df_polars.lazy()
        .filter(
            (pl.col(&quot;name&quot;) == &quot;David&quot;)
            &amp;amp; (pl.col(&quot;born&quot;) &amp;gt; 1980)
            &amp;amp; (pl.col(&quot;city&quot;) != &quot;London&quot;)
            | (pl.col(&quot;is_married&quot;) == True) &amp;amp; (pl.col(&quot;children&quot;) &amp;gt;= 3)
        )
        .collect()
    ),
}
filter_row_multiple_condition = run_test(test_dict, &quot;Filter (complex)&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;Both libraries tackled this challenge quite well, yet Pandas struggled to keep pace with Polars. It&apos;s intriguing to observe that while Pandas required nearly twice the time for the more intricate task, Polars managed to complete it in almost the same amount of time. Parallelization in action.&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h1&gt;Performing operations on columns&lt;/h1&gt;
&lt;p&gt;Now, let&apos;s roll up our sleeves and dive into performing some operations on the columns.&lt;/p&gt;
&lt;h2&gt;Single operation&lt;/h2&gt;
&lt;p&gt;As a single operation, we&apos;ll simply calculate the century in which these individuals were born.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
test_dict = {
    &quot;Pandas&quot;: lambda: (df_pandas.assign(born=lambda df: df.born.div(100).round())),
    &quot;Polars&quot;: lambda: (
        df_polars.lazy().with_columns((pl.col(&quot;born&quot;) / 100).round()).collect()
    ),
}
operate_one_column = run_test(test_dict, &quot;Operate (one column)&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Multiple operations&lt;/h2&gt;
&lt;p&gt;Let&apos;s also explore what happens when performing multiple operations on the columns. We&apos;ll mix things up with some string operations, mapping, and math calculations to see how these libraries handle it!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
test_dict = {
    &quot;Pandas&quot;: lambda: (
        df_pandas.assign(
            born=lambda df: df.born.div(100).round(),
            name=lambda df: df.name.str.lower(),
            city=lambda df: df.city.str.upper(),
            zip_code=lambda df: df.zip_code.mul(2),
            income=lambda df: df.income.div(10).astype(&quot;str&quot;),
            is_married=lambda df: df.is_married.map({False: 0, True: 1}),
            children=lambda df: df.children.astype(&quot;bool&quot;),
            car=lambda df: df.car.str[0],
        )
    ),
    &quot;Polars&quot;: lambda: (
        df_polars.lazy()
        .with_columns(
            [
                (pl.col(&quot;born&quot;) / 100).round(),
                pl.col(&quot;name&quot;).str.to_lowercase(),
                pl.col(&quot;city&quot;).str.to_uppercase(),
                pl.col(&quot;zip_code&quot;) * 2,
                (pl.col(&quot;income&quot;) / 10).cast(pl.Utf8),
                pl.col(&quot;is_married&quot;).map_dict({False: 0, True: 1}),
                pl.col(&quot;children&quot;).cast(pl.Boolean),
                pl.col(&quot;car&quot;).str.slice(0, length=1),
            ]
        )
        .collect()
    ),
}
operate_multiple_column = run_test(test_dict, &quot;Operate (more columns)&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;Once again, Polars takes the lead. While both libraries required more time for the task involving multiple operations, Polars demonstrated superior scalability in this scenario.&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h1&gt;Concatenating Data&lt;/h1&gt;
&lt;p&gt;Now, let&apos;s turn our attention to concatenating two datasets. Let the merging begin!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df_pandas2 = df_pandas.copy(deep=True)
df_polars2 = df_polars.clone()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
test_dict = {
    &quot;Pandas&quot;: lambda: pd.concat([df_pandas, df_pandas2], axis=0),
    &quot;Polars&quot;: lambda: pl.concat([df_polars, df_polars2], how=&quot;vertical&quot;),
}
concatenate = run_test(test_dict, &quot;Concatenate&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;Once more, Polars shines with a remarkable speed advantage.&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h1&gt;Aggregation&lt;/h1&gt;
&lt;h2&gt;Simple&lt;/h2&gt;
&lt;p&gt;Time to shift our attention to aggregation. First up, a simple task: let&apos;s calculate the mean income based on names. Then, for a bit more complexity, we&apos;ll dive into computing statistics involving the income, children, and car columns. Things are about to get interesting!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
test_dict = {
    &quot;Pandas&quot;: lambda: (df_pandas.groupby(&quot;name&quot;).income.mean()),
    &quot;Polars&quot;: lambda: (df_polars.lazy().groupby(&quot;name&quot;).mean().collect()),
}
aggregate_simple = run_test(test_dict, &quot;Aggregate (simple)&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;More complex&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;
test_dict = {
    &quot;Pandas&quot;: lambda: (
        df_pandas.groupby([&quot;name&quot;, &quot;car&quot;, &quot;is_married&quot;]).agg(
            born_min=(&quot;born&quot;, min),
            born_max=(&quot;born&quot;, max),
            income_mean=(&quot;income&quot;, np.mean),
            income_median=(&quot;income&quot;, np.median),
            children_mean=(&quot;children&quot;, np.mean),
            car_count=(&quot;car&quot;, &quot;count&quot;),
        )
    ),
    &quot;Polars&quot;: lambda: (
        df_polars.lazy()
        .groupby([&quot;name&quot;, &quot;car&quot;, &quot;is_married&quot;])
        .agg(
            [
                pl.col(&quot;born&quot;).min().alias(&quot;born_min&quot;),
                pl.col(&quot;born&quot;).max().alias(&quot;born_max&quot;),
                pl.col(&quot;income&quot;).mean().alias(&quot;income_mean&quot;),
                pl.col(&quot;income&quot;).median().alias(&quot;income_median&quot;),
                pl.col(&quot;children&quot;).mean().alias(&quot;children_mean&quot;),
                pl.col(&quot;car&quot;).count().alias(&quot;car_count&quot;),
            ]
        )
        .collect()
    ),
}
aggregate_complex = run_test(test_dict, &quot;Aggregate (complex)&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;While Pandas showcased noteworthy speed for the simple aggregation, the more intricate task exposed significant disparities between the two libraries. Polars took a commanding lead in this scenario, presenting a considerably faster performance compared to Pandas.
:::&lt;/p&gt;
&lt;h1&gt;Whole workflow&lt;/h1&gt;
&lt;p&gt;We&apos;re going to tackle this in two versions. First, we&apos;ll use &lt;code&gt;pl.read_csv&lt;/code&gt; for Polars, assessing the time it takes to complete the entire workflow on a DataFrame already in memory. Then, for the second version, we&apos;ll employ &lt;code&gt;pl.scan_csv&lt;/code&gt; for Polars. This nifty function lets us lazily read data from a CSV file or multiple files using glob patterns.&lt;/p&gt;
&lt;p&gt;The cool part? With &lt;code&gt;pl.scan_csv&lt;/code&gt;, the query optimizer can push down predicates and projections to the scan level. This nifty move has the potential to cut down on memory overhead. Let&apos;s dive into both versions and see how they stack up!&lt;/p&gt;
&lt;h2&gt;Using &lt;code&gt;pl.read_csv&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;
test_dict = {
    &quot;Pandas&quot;: lambda: (
        df_pandas.loc[:, lambda df: ~df.columns.isin([&quot;is_married&quot;])]
        .query(&quot;name==&apos;David&apos; and born&amp;gt;1980 and city != &apos;London&apos; and children &amp;gt;= 3&quot;)
        .assign(
            born=lambda df: df.born.div(100).round(),
            name=lambda df: df.name.str.lower(),
            city=lambda df: df.city.str.upper(),
            zip_code=lambda df: df.zip_code.mul(2),
            income=lambda df: df.income.div(10),
            children=lambda df: df.children.astype(&quot;bool&quot;),
            car=lambda df: df.car.str[0],
        )
        .groupby(
            [
                &quot;name&quot;,
                &quot;car&quot;,
            ]
        )
        .agg(
            born_min=(&quot;born&quot;, min),
            born_max=(&quot;born&quot;, max),
            income_mean=(&quot;income&quot;, np.mean),
            income_median=(&quot;income&quot;, np.median),
            children_mean=(&quot;children&quot;, np.mean),
            car_count=(&quot;car&quot;, &quot;count&quot;),
        )
    ),
    &quot;Polars&quot;: lambda: (
        df_polars.lazy()
        .select(cs.all() - cs.ends_with(&quot;married&quot;))
        .filter(
            (pl.col(&quot;name&quot;) == &quot;David&quot;)
            &amp;amp; (pl.col(&quot;born&quot;) &amp;gt; 1980)
            &amp;amp; (pl.col(&quot;city&quot;) != &quot;London&quot;)
            &amp;amp; (pl.col(&quot;children&quot;) &amp;gt;= 3)
        )
        .with_columns(
            [
                (pl.col(&quot;born&quot;) / 100).round(),
                pl.col(&quot;name&quot;).str.to_lowercase(),
                pl.col(&quot;city&quot;).str.to_uppercase(),
                pl.col(&quot;zip_code&quot;) * 2,
                (pl.col(&quot;income&quot;) / 10),
                pl.col(&quot;children&quot;).cast(pl.Boolean),
                pl.col(&quot;car&quot;).str.slice(0, length=1),
            ]
        )
        .groupby(
            [
                &quot;name&quot;,
                &quot;car&quot;,
            ]
        )
        .agg(
            [
                pl.col(&quot;born&quot;).min().alias(&quot;born_min&quot;),
                pl.col(&quot;born&quot;).max().alias(&quot;born_max&quot;),
                pl.col(&quot;income&quot;).mean().alias(&quot;income_mean&quot;),
                pl.col(&quot;income&quot;).median().alias(&quot;income_median&quot;),
                pl.col(&quot;children&quot;).mean().alias(&quot;children_mean&quot;),
                pl.col(&quot;car&quot;).count().alias(&quot;car_count&quot;),
            ]
        )
        .collect()
    ),
}
whole_workflow_read_csv = run_test(test_dict, &quot;Whole workflow&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Using &lt;code&gt;pl.scan_csv&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;
test_dict = {
    &quot;Pandas&quot;: lambda: (
        df_pandas.loc[:, lambda df: ~df.columns.isin([&quot;is_married&quot;])]
        .query(&quot;name==&apos;David&apos; and born&amp;gt;1980 and city != &apos;London&apos; and children &amp;gt;= 3&quot;)
        .assign(
            born=lambda df: df.born.div(100).round(),
            name=lambda df: df.name.str.lower(),
            city=lambda df: df.city.str.upper(),
            zip_code=lambda df: df.zip_code.mul(2),
            income=lambda df: df.income.div(10),
            children=lambda df: df.children.astype(&quot;bool&quot;),
            car=lambda df: df.car.str[0],
        )
        .groupby(
            [
                &quot;name&quot;,
                &quot;car&quot;,
            ]
        )
        .agg(
            born_min=(&quot;born&quot;, min),
            born_max=(&quot;born&quot;, max),
            income_mean=(&quot;income&quot;, np.mean),
            income_median=(&quot;income&quot;, np.median),
            children_mean=(&quot;children&quot;, np.mean),
            car_count=(&quot;car&quot;, &quot;count&quot;),
        )
    ),
    &quot;Polars&quot;: lambda: (
        pl.scan_csv(&quot;sample.csv&quot;)
        .select(cs.all() - cs.ends_with(&quot;married&quot;))
        .filter(
            (pl.col(&quot;name&quot;) == &quot;David&quot;)
            &amp;amp; (pl.col(&quot;born&quot;) &amp;gt; 1980)
            &amp;amp; (pl.col(&quot;city&quot;) != &quot;London&quot;)
            &amp;amp; (pl.col(&quot;children&quot;) &amp;gt;= 3)
        )
        .with_columns(
            [
                (pl.col(&quot;born&quot;) / 100).round(),
                pl.col(&quot;name&quot;).str.to_lowercase(),
                pl.col(&quot;city&quot;).str.to_uppercase(),
                pl.col(&quot;zip_code&quot;) * 2,
                (pl.col(&quot;income&quot;) / 10),
                pl.col(&quot;children&quot;).cast(pl.Boolean),
                pl.col(&quot;car&quot;).str.slice(0, length=1),
            ]
        )
        .groupby(
            [
                &quot;name&quot;,
                &quot;car&quot;,
            ]
        )
        .agg(
            [
                pl.col(&quot;born&quot;).min().alias(&quot;born_min&quot;),
                pl.col(&quot;born&quot;).max().alias(&quot;born_max&quot;),
                pl.col(&quot;income&quot;).mean().alias(&quot;income_mean&quot;),
                pl.col(&quot;income&quot;).median().alias(&quot;income_median&quot;),
                pl.col(&quot;children&quot;).mean().alias(&quot;children_mean&quot;),
                pl.col(&quot;car&quot;).count().alias(&quot;car_count&quot;),
            ]
        )
        .collect()
    ),
}
whole_workflow_scan_csv = run_test(test_dict, &quot;Whole workflow (scan_csv)&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As evident, the utilization of &lt;code&gt;scan_csv&lt;/code&gt; increased the required time by about 3-4 times. However, even with this increase, Polars still manages to maintain a substantial advantage of around 5 times faster than the entire workflow executed using Pandas.&lt;/p&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;When we consider the entirety of the data processing pipeline, irrespective of the file reading approach, Polars emerges as the victor. It consistently exhibits a considerable speed advantage compared to Pandas.&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h1&gt;Putting it all together&lt;/h1&gt;
&lt;p&gt;Time to bring together all the things we&apos;ve explored! Let&apos;s sum up what we&apos;ve learned.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;

summary = pd.concat(
    [
        read_csv,
        read_parquet,
        # select_col_not_in_memory,
        select_col_in_memory,
        filter_row_one_condition,
        filter_row_multiple_condition,
        operate_one_column,
        operate_multiple_column,
        concatenate,
        aggregate_simple,
        aggregate_complex,
        whole_workflow_read_csv,
        # whole_workflow_scan_csv
    ],
    axis=1,
)

fig = (
    summary.melt(var_name=&quot;experiment&quot;, value_name=&quot;time_sec&quot;)
    .assign(
        package=lambda df: df.experiment.str[-6:],
        experiment=lambda df: df.experiment.str[:-7],
    )
    .groupby([&quot;experiment&quot;, &quot;package&quot;])
    .time_sec.mean()
    .mul(1000)
    .reset_index()
    .pivot(columns=&quot;package&quot;, values=&quot;time_sec&quot;, index=&quot;experiment&quot;)
    .sort_values(
        by=&quot;polars&quot;,
    )
    .reset_index()
    .pipe(
        lambda df: px.bar(
            df,
            y=&quot;experiment&quot;,
            x=[&quot;pandas&quot;, &quot;polars&quot;],
            barmode=&quot;group&quot;,
            text_auto=&apos;.2s&apos;,
            orientation=&quot;h&quot;,
            labels={&quot;value&quot;: &quot;Average execution Time (msec)&quot;, &quot;experiment&quot;: &quot;&quot;},
            title= f&apos;&amp;lt;b&amp;gt;Speed Showdown in Data Workflow Operations&amp;lt;/b&amp;gt; &amp;lt;br&amp;gt;&amp;lt;sup&amp;gt;&amp;lt;i&amp;gt;Pandas {pd.__version__} vs. Polars {pl.__version__}&amp;lt;/i&amp;gt;&amp;lt;/sup&amp;gt;&apos;,
            width=1000,
            height=700,
        )
    )
)
fig.update_xaxes(tickangle=90)
fig.update_layout(yaxis=dict(automargin=True), legend_title_text=&quot;Library&quot;)
fig.update_traces(textposition=&apos;outside&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Throughout our exploration, Polars has consistently outperformed Pandas. We intentionally focused on tasks that frequently arise in data analysis, encompassing file reading, column selection, filtering, and more. However, where Polars truly shines is in aggregation, capitalizing on its multiprocessing prowess. Take a glance at @fig-fig14, and you&apos;ll notice that reading files is the most time-consuming step for Polars. To address this (and reduce memory usage at the same time), we have the nifty &lt;code&gt;scan_csv&lt;/code&gt; function that enables Polars to operate in a lazy mode, optimizing the entire data pipeline. It&apos;s clear that Polars packs a punch in the realm of data processing!&lt;/p&gt;
&lt;p&gt;Should you consider using Polars? Based on the findings of this experiment, it&apos;s evident that Polars can bring substantial benefits to every stage of your data processing. Its remarkable speed can make a significant impact. If you&apos;re open to investing time in understanding its API (which isn&apos;t drastically different from Pandas), I&apos;m confident your projects will gain a significant speed boost. And don&apos;t forget, the &lt;a href=&quot;https://www.crunchbase.com/funding_round/polars-seed--963c89d2&quot;&gt;trajectory&lt;/a&gt; of the project seems promising, implying a bright future ahead.&lt;/p&gt;
&lt;p&gt;Whatever path you choose, ensure you become well-acquainted with your selected library and stay current with emerging technologies. And above all, relish the journey. Happy coding, and until next time! 🐼🐍🤓💻&lt;/p&gt;
</content:encoded><author>Adam Cseresznye</author></item><item><title>Tidy Tuesday: Spam E-mail</title><link>https://adamcseresznye.github.io/posts/spam</link><guid isPermaLink="true">https://adamcseresznye.github.io/posts/spam</guid><pubDate>Tue, 15 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;spam.jpg&quot; alt=&quot;Photo by Stephen Phillips - Hostreviews.co.uk on UnSplash&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Welcome to this week&apos;s Tidy Tuesday! Today, we&apos;re delving into the intriguing yet bothersome realm of email spams. You know, those unsolicited messages that flood our inboxes? They go by many names like junk email or spam mail, and they&apos;re sent in bulk. The term &quot;spam&quot; got its name from a Monty Python sketch where the word &quot;Spam&quot; was everywhere, just like these emails. Starting from the early 1990s, these spam messages have been on a steady rise, making up around 90% of all email traffic by 2014.&lt;/p&gt;
&lt;p&gt;We&apos;ve got our data from the Tidy Tuesday treasure trove over at &lt;a href=&quot;https://github.com/rfordatascience/tidytuesday/tree/master/data/2023/2023-08-15&quot;&gt;GitHub&lt;/a&gt;!  This dataset, from Vincent Arel-Bundock&apos;s Rdatasets package, was initially gathered at Hewlett-Packard Labs. They later kindly shared it with the UCI Machine Learning Repository.&lt;/p&gt;
&lt;p&gt;This treasure trove of information consists of 4601 emaily sorted into spam and non-spam categorie&lt;/p&gt;
&lt;h1&gt;Setup&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;import polars as pl
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import polars.selectors as cs
import numpy as np
from sklearn import preprocessing
from sklearn import decomposition


from lets_plot import *
from lets_plot.mapping import as_discrete

LetsPlot.setup_html()

import plotly.io as pio
import plotly.express as px

pio.templates.default = &quot;presentation&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;df = pl.read_csv(
    &quot;https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2023/2023-08-15/spam.csv&quot;
).with_columns(
    [
        pl.col(&quot;crl.tot&quot;).cast(pl.Int16),
        pl.col(&quot;dollar&quot;).cast(pl.Float32),
        pl.col(&quot;bang&quot;).cast(pl.Float32),
        pl.col(&quot;money&quot;).cast(pl.Float32),
        pl.col(&quot;n000&quot;).cast(pl.Float32),
        pl.col(&quot;make&quot;).cast(pl.Float32),
        pl.col(&quot;yesno&quot;).map_dict({&quot;y&quot;: 1, &quot;n&quot;: 0}).cast(pl.Int8),
    ]
)

df.sample(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the data dictionary:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;crl.tot&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Total length of uninterrupted sequences of capitals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dollar&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Occurrences of the dollar sign, as percent of total number of characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bang&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Occurrences of ‘!’, as percent of total number of characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;money&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Occurrences of ‘money’, as percent of total number of characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n000&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Occurrences of the string ‘000’, as percent of total number of words&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;make&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Occurrences of ‘make’, as a percent of total number of words&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;yesno&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Outcome variable, a factor with levels &apos;n&apos; not spam, &apos;y&apos; spam&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Looking at the data dictionary, a few intriguing questions arise:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How is the distribution between True and False answers? Are non-spam emails significantly more prevalent?&lt;/li&gt;
&lt;li&gt;What are the top words that frequently appear in spam emails?&lt;/li&gt;
&lt;li&gt;Can we explore the relative frequency distribution disparities of words linked with spam? What&apos;s the increased likelihood of finding these specific words in a spam email versus a non-spam one?&lt;/li&gt;
&lt;li&gt;Is there an overarching correlation within the dataset? Are certain words frequently seen together?&lt;/li&gt;
&lt;li&gt;Could we pinpoint the ultimate worst spam email in the dataset?&lt;/li&gt;
&lt;li&gt;Would it be feasible to employ principal component analysis to visualize this dataset effectively?&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;What&apos;s the distribution of True and False answers&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;(
    df.select(pl.col(&quot;yesno&quot;))
    .to_series()
    .value_counts()
    .with_columns((pl.col(&quot;counts&quot;) / pl.sum(&quot;counts&quot;)).alias(&quot;percent_count&quot;))
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
As you&apos;ve observed, the dataset displays a slight imbalance, with spam emails constituting approximately 40% of the dataset.
:::&lt;/p&gt;
&lt;h1&gt;What are the most frequently used words in spam emails?&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;
fig = (
    df.filter(pl.col(&quot;yesno&quot;) == 1)
    .select(cs.float())
    .melt()
    .groupby(&quot;variable&quot;)
    .agg(
        pl.col(&quot;value&quot;).mean().alias(&quot;mean&quot;),
    )
    .sort(by=&quot;mean&quot;)
    .pipe(
        lambda df: px.bar(
            df,
            x=&quot;variable&quot;,
            y=&quot;mean&quot;,
            color=&quot;variable&quot;,
            width=600,
            height=500,
            labels={
                &quot;mean&quot;: &quot;Average % occurrence per total word count&quot;,
                &quot;variable&quot;: &quot;&quot;,
            },
        )
    )
)
fig.update_traces(hovertemplate=&quot;&amp;lt;br&amp;gt;Occurence: %{y:.2f}&quot;)  #
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
Among the frequently used words, &quot;bang&quot; takes a noticeable spot, appearing on average in 0.51% of the total word count.&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h1&gt;What are the relative frequency distribution differences of the words associated with spam?&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;
fig = (
    df.select(pl.all().exclude(&quot;crl.tot&quot;))
    .groupby(&quot;yesno&quot;)
    .agg(
        pl.all().mean(),
    )
    .to_pandas()
    .apply(lambda x: x / x.sum(), axis=0)
    .melt(id_vars=&quot;yesno&quot;)
    .rename(columns={&quot;yesno&quot;: &quot;spam&quot;})
    .assign(spam=lambda df: df.spam.map({0: &quot;Not a spam&quot;, 1: &quot;Spam&quot;}))
    .assign(percentage=lambda x: [&quot;{:.2%}&quot;.format(val) for val in x[&quot;value&quot;]])
    .pipe(
        lambda df: px.bar(
            df,
            x=&quot;variable&quot;,
            y=&quot;value&quot;,
            color=&quot;spam&quot;,
            labels={
                &quot;value&quot;: &quot;Relative frequency of words compared to class &quot;,
                &quot;variable&quot;: &quot;&quot;,
            },
            text=&quot;percentage&quot;,
            width=700,
            height=600,
        )
    )
)

fig.update_layout(
    showlegend=True, uniformtext_minsize=8, uniformtext_mode=&quot;hide&quot;, hovermode=False
)
fig.update_traces(
    textposition=&quot;inside&quot;,
)
fig.update_yaxes(tickformat=&quot;.0%&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
The word &quot;n000&quot; is 36x more likely to appear in a spam email.
:::&lt;/p&gt;
&lt;h1&gt;What&apos;s the overall correlation&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;
fig = (
    df.to_pandas()
    .corr()
    .round(2)
    .pipe(
        lambda df: px.imshow(
            df,
            text_auto=True,
            aspect=&quot;equal&quot;,
            zmin=-1,
            zmax=1,
            color_continuous_scale=&quot;RdBu_r&quot;,
            # origin=&apos;lower&apos;,
            width=600,
            height=600,
        )
    )
)
fig.update_xaxes(side=&quot;top&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
Our target variable, &quot;yesno,&quot; exhibits the strongest moderate positive correlation with the occurrence of &quot;n000.&quot;
:::&lt;/p&gt;
&lt;h1&gt;What is the worst spam email?&lt;/h1&gt;
&lt;p&gt;To identify the most significant culprit within the dataset, we&apos;ll sum up the numerical percentage values and then sort the values accordingly.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(
    df.filter(pl.col(&quot;yesno&quot;) == 1)
    .with_columns(pl.sum_horizontal(pl.all().exclude(&quot;crl.tot&quot;, &quot;yesno&quot;)))
    .sort(by=&quot;sum&quot;, descending=True)
    .head()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
The most concerning spam email in our dataset showcases a total occurrence of 3% for the word &quot;bang,&quot; 9% for &quot;money,&quot; and 4.5% for &quot;make.&quot; Yikes.
:::&lt;/p&gt;
&lt;h1&gt;Visualize data with PCA&lt;/h1&gt;
&lt;p&gt;To enhance our grasp of the data, we can employ Principal Component Analysis (PCA). This technique comes in handy when we&apos;re dealing with multiple independent variables, allowing us to reduce dimensionality and streamline the dataset.&lt;/p&gt;
&lt;h2&gt;Visualize all the principal components&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;X = df.select(pl.all().exclude(&quot;yesno&quot;)).to_pandas()
y = df.select(pl.col(&quot;yesno&quot;)).to_pandas()
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Before PCA we need to scale and transform our dataset

scaler = preprocessing.PowerTransformer().set_output(transform=&quot;pandas&quot;)

X = scaler.fit_transform(X)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
pca = decomposition.PCA()
components = pca.fit_transform(X)
labels = {
    str(i): f&quot;PC {i+1} ({var:.1f}%)&quot;
    for i, var in enumerate(pca.explained_variance_ratio_ * 100)
}

fig = px.scatter_matrix(
    components,
    opacity=0.2,
    labels=labels,
    dimensions=range(6),
    color=y.squeeze().map({0: &quot;Not a Spam&quot;, 1: &quot;Spam&quot;}),
    width=1000,
    height=1000,
)
fig.update_traces(diagonal_visible=False)
fig.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
As evident, the first and second principal components manage to account for 62% of the variance within the data. A distinct separation of classes is noticeable, particularly with the spam group exhibiting a broader distribution, indicating greater diversity.
:::&lt;/p&gt;
&lt;h2&gt;Visualize Loadings&lt;/h2&gt;
&lt;p&gt;For a deeper comprehension of how each characteristic influences our principal components, we can delve into examining the loadings.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
pca = decomposition.PCA(n_components=2)
components = pca.fit_transform(X)
loadings = pca.components_.T * np.sqrt(pca.explained_variance_)


fig = px.scatter(
    components,
    x=0,
    y=1,
    color=y.squeeze().map({0: &quot;Not a Spam&quot;, 1: &quot;Spam&quot;}),
    labels={
        &quot;0&quot;: f&quot;PC1 ({pca.explained_variance_ratio_[0]:.0%})&quot;,
        &quot;1&quot;: f&quot;PC2 ({pca.explained_variance_ratio_[1]:.0%})&quot;,
    },
    template=&quot;plotly_dark&quot;,
    color_discrete_sequence=[
        &quot;red&quot;,
        &quot;green&quot;,
    ],
    opacity=0.4,
    width=700,
    height=700,
)
for i, feature in enumerate(X.columns):
    fig.add_annotation(
        ax=0,
        ay=0,
        axref=&quot;x&quot;,
        ayref=&quot;y&quot;,
        x=loadings[i, 0],
        y=loadings[i, 1],
        showarrow=True,
        arrowsize=1,
        arrowhead=2,
        xanchor=&quot;right&quot;,
        yanchor=&quot;top&quot;,
    )
    fig.add_annotation(
        x=loadings[i, 0],
        y=loadings[i, 1],
        ax=0,
        ay=0,
        font=dict(
            size=20,
            # color=&apos;yellow&apos;
        ),
        xanchor=&quot;left&quot;,
        yanchor=&quot;bottom&quot;,
        text=feature,
        yshift=5,
    )
fig
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;The loadings plot reveals a strong correlation among the variables &quot;crl.tot,&quot; &quot;money,&quot; and &quot;n000&quot;. If we were to construct a machine learning model for spam email recognition, we could opt for one of these variables to streamline our dataset. Furthermore, the disassociation of &quot;bang&quot; from this trio is evident, its vector positioned at a 90° angle.
:::&lt;/p&gt;
&lt;p&gt;Alright folks, our journey into the intriguing realm of spam emails has left us with some valuable insights. It&apos;s clear that words like &quot;bang&quot; and &quot;n000&quot; are key indicators to watch out for. Luckily, modern machine learning models are here to do the hard work on our behalf.&lt;/p&gt;
&lt;p&gt;Stay vigilant out there and take care! See you in the next week&apos;s adventure! 👋📧🛡️&lt;/p&gt;
</content:encoded><author>Adam Cseresznye</author></item><item><title>Tidy Tuesday: Refugees</title><link>https://adamcseresznye.github.io/posts/refugee</link><guid isPermaLink="true">https://adamcseresznye.github.io/posts/refugee</guid><pubDate>Tue, 22 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://assets.nst.com.my/images/articles/vibes061220lucienrefugeeb_1607218243.jpg&quot; alt=&quot;Teowodros Hagos&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Welcome to our blog dedicated to &lt;a href=&quot;https://github.com/rfordatascience/tidytuesday/tree/master/data/2023/2023-08-22&quot;&gt;Tidy Tuesday&lt;/a&gt;. This week, we venture into the sobering realm of refugee statistics using the &lt;a href=&quot;https://cran.r-project.org/web/packages/refugees/index.html&quot;&gt;{refugees}&lt;/a&gt; R package. This tool grants access to the United Nations High Commissioner for Refugees (UNHCR) Refugee Data Finder, providing critical insights into forcibly displaced populations spanning over seven decades. With data from UNHCR, UNRWA, and the Internal Displacement Monitoring Centre, we&apos;ll explore a subset of this information, focusing on population statistics from 2010 to 2022.&lt;/p&gt;
&lt;p&gt;Join us as we explore the landscape of refugee data.&lt;/p&gt;
&lt;h1&gt;Setup&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import numpy as np

from pathlib import Path
import json
from urllib.request import urlopen


from lets_plot import *
from lets_plot.mapping import as_discrete
from plotly.subplots import make_subplots
import plotly.graph_objects as go

LetsPlot.setup_html()

import plotly.io as pio
import plotly.express as px

pio.templates.default = &quot;presentation&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;try:
    df = pd.read_csv(
        &quot;https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2023/2023-08-22/population.csv&quot;
    )
except:
    df = pd.read_csv(
        Path.cwd().joinpath(
            &quot;raw.githubusercontent.com_rfordatascience_tidytuesday_master_data_2023_2023-08-22_population.csv&quot;
        )
    )

df = (
    df
    # .assign(year=lambda df: pd.to_datetime(df.year, format=&apos;%Y&apos;))
)

df.sample(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the data dictionary:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;variable&lt;/th&gt;
&lt;th&gt;class&lt;/th&gt;
&lt;th&gt;description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;year&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;The year.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;coo_name&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Country of origin name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;coo&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Country of origin UNHCR code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;coo_iso&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Country of origin ISO code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;coa_name&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Country of asylum name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;coa&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Country of asylum UNHCR code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;coa_iso&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Country of asylum ISO code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;refugees&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;The number of refugees.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;asylum_seekers&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;The number of asylum-seekers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;returned_refugees&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;The number of returned refugees.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;idps&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;The number of internally displaced persons.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;returned_idps&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;The number of returned internally displaced persons.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stateless&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;The number of stateless persons.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ooc&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;The number of others of concern to UNHCR.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;oip&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;The number of other people in need of international protection.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hst&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;The number of host community members.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Looking at the data dictionary, here are the questions we will be exploring in this blog post:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How has the overall refugee count evolved over the years?&lt;/li&gt;
&lt;li&gt;What countries experience the highest annual refugee outflows?&lt;/li&gt;
&lt;li&gt;What countries receive the most refugees annually?&lt;/li&gt;
&lt;li&gt;Where do most refugees from the country with the highest numbers go?&lt;/li&gt;
&lt;li&gt;Which countries have the highest numbers of internally displaced persons?&lt;/li&gt;
&lt;li&gt;What countries have the highest stateless populations?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this blog, to visualize the topological data, we will be utilizing Plotly&apos;s &lt;code&gt;choropleth_mapbox&lt;/code&gt; function as well as &lt;code&gt;scatter_geo&lt;/code&gt;. For a quick overview about Plotly check out this &lt;a href=&quot;https://plotly.com/python/plotly-express/&quot;&gt;website&lt;/a&gt;. In addition, our GeoJSON file, which represents simple geographical features along with their non-spatial attributes, is sourced from &lt;a href=&quot;https://github.com/datasets/geo-boundaries-world-110m/blob/master/countries.geojson&quot;&gt;GitHub&lt;/a&gt; curated by Rufus Pollock.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    with urlopen(
        &quot;https://raw.githubusercontent.com/datasets/geo-boundaries-world-110m/master/countries.geojson&quot;
    ) as response:
        countries = json.load((response))
except:
    countries = json.load(open(&quot;countries.geojson&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;How has the overall refugee count evolved over the years?&lt;/h1&gt;
&lt;p&gt;As you can see, the number of stateless persons has not changed significantly over the period studied; on the other hand, the number of internally displaced persons has dramatically increased from over 14 million in 2010 to just shy of 60 million by 2022&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
fig = (
    df.groupby(&quot;year&quot;)[[&quot;refugees&quot;, &quot;asylum_seekers&quot;, &quot;stateless&quot;, &quot;idps&quot;]]
    .sum()
    .reset_index()
    .melt(id_vars=&quot;year&quot;)
    .pipe(
        lambda df: px.line(
            df,
            x=df.year,
            y=df.value,
            color=df.variable,
            width=750,
            height=600,
            # markers=True,
            labels={
                &quot;year&quot;: &quot;Year&quot;,
                &quot;value&quot;: &quot;Number of individuals&quot;,
            },
        )
    )
)
fig.update_layout(legend_title_text=&quot;Categories&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;What countries experience the highest annual refugee outflows?&lt;/h1&gt;
&lt;p&gt;Among all the countries under examination, the highlighted ones represent the top 5 nations with the highest registered refugee populations. It&apos;s evident that the Syrian Arab Republic consistently maintained the highest number of registered refugees from 2013 to 2022, followed closely by Afghanistan&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
fig = go.Figure()

top10_countries = (
    df.groupby(&quot;coo_name&quot;)
    .refugees.sum()
    .to_frame()
    .reset_index()
    .sort_values(by=&quot;refugees&quot;, ascending=False)
    .head(5)
    .coo_name.values
)

test = (
    df.groupby([&quot;year&quot;, &quot;coo_name&quot;])
    .refugees.sum()
    .to_frame()
    .reset_index()
    .assign(
        color=lambda df: df.coo_name.mask(
            df.coo_name == (top10_countries[0]), &quot;#6eb14a&quot;
        )
        .mask(df.coo_name == (top10_countries[1]), &quot;#2d4526&quot;)
        .mask(df.coo_name == (top10_countries[2]), &quot;#f6d19b&quot;)
        .mask(df.coo_name == (top10_countries[3]), &quot;#ca3780&quot;)
        .mask(df.coo_name == (top10_countries[4]), &quot;#e9b4cd&quot;)
        .mask(~df.coo_name.isin(top10_countries), &quot;#dadadc&quot;)
    )
)

# Loop over each unique &apos;coo_name&apos;
for coo_name in test[&quot;coo_name&quot;].unique():
    df_subset = test[test[&quot;coo_name&quot;] == coo_name]
    if coo_name in top10_countries:
        fig.add_trace(
            go.Scatter(
                x=df_subset[&quot;year&quot;],
                y=df_subset[&quot;refugees&quot;],
                mode=&quot;lines+markers&quot;,
                name=coo_name,
                showlegend=True,
                line=dict(
                    color=df_subset[&quot;color&quot;].iloc[0], dash=&quot;dash&quot;
                ),  # Use the first color from the subset
            )
        )
    else:
        fig.add_trace(
            go.Scatter(
                x=df_subset[&quot;year&quot;],
                y=df_subset[&quot;refugees&quot;],
                mode=&quot;lines&quot;,
                name=coo_name,
                showlegend=False,
                line=dict(
                    color=df_subset[&quot;color&quot;].iloc[0], dash=&quot;dot&quot;
                ),  # Use the first color from the subset
            )
        )


fig.update_layout(
    autosize=False, width=750, height=600, yaxis_title=&quot;Sum Annual Refugee Outflows&quot;
)
fig.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I have also prepared a short animation that depicts the year-by-year pattern of refugee outflows.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    df.assign(year=lambda df: df.year.astype(str).str.split(&quot;-&quot;, expand=True)[0])
    .groupby([&quot;year&quot;, &quot;coo_iso&quot;, &quot;coo_name&quot;])[&quot;refugees&quot;]
    .sum()
    .to_frame()
    .reset_index()
    .pipe(
        lambda df: px.choropleth_mapbox(
            data_frame=df,
            locations=&quot;coo_iso&quot;,
            geojson=countries,
            color=&quot;refugees&quot;,
            animation_frame=&quot;year&quot;,
            animation_group=&quot;coo_iso&quot;,
            hover_name=&quot;coo_name&quot;,
            hover_data={
                &quot;coo_iso&quot;: &quot;&quot;,
            },
            range_color=(0, 8_000_000),
            color_continuous_scale=px.colors.sequential.Oranges,
            featureidkey=&quot;properties.iso_a3&quot;,
            zoom=1.2,
            mapbox_style=&quot;carto-positron&quot;,
            opacity=0.6,
            width=1200,
            height=700
            # **fig_dict
        )
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;What countries receive the most refugees annually?&lt;/h1&gt;
&lt;p&gt;After examining which countries have the most refugees leaving, we will shift our focus to the countries with the highest number of refugees received.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;

fig = go.Figure()

top5_countries_accept = (
    df.groupby(&quot;coa_name&quot;)[[&quot;refugees&quot;]]
    .sum()
    .sort_values(by=&quot;refugees&quot;, ascending=False)
    .head(5)
    .index
)

test = (
    df.groupby([&quot;year&quot;, &quot;coa_name&quot;])
    .refugees.sum()
    .to_frame()
    .reset_index()
    .assign(
        color=lambda df: df.coa_name.mask(
            df.coa_name == (top5_countries_accept[0]), &quot;#6eb14a&quot;
        )
        .mask(df.coa_name == (top5_countries_accept[1]), &quot;#2d4526&quot;)
        .mask(df.coa_name == (top5_countries_accept[2]), &quot;#f6d19b&quot;)
        .mask(df.coa_name == (top5_countries_accept[3]), &quot;#ca3780&quot;)
        .mask(df.coa_name == (top5_countries_accept[4]), &quot;#e9b4cd&quot;)
        .mask(~df.coa_name.isin(top5_countries_accept), &quot;#dadadc&quot;)
    )
)

# Loop over each unique &apos;coo_name&apos;
for coa_name in test[&quot;coa_name&quot;].unique():
    df_subset = test[test[&quot;coa_name&quot;] == coa_name]
    if coa_name in top5_countries_accept:
        fig.add_trace(
            go.Scatter(
                x=df_subset[&quot;year&quot;],
                y=df_subset[&quot;refugees&quot;],
                mode=&quot;lines+markers&quot;,
                name=coa_name,
                showlegend=True,
                line=dict(
                    color=df_subset[&quot;color&quot;].iloc[0], dash=&quot;dash&quot;
                ),  # Use the first color from the subset
            )
        )
    else:
        fig.add_trace(
            go.Scatter(
                x=df_subset[&quot;year&quot;],
                y=df_subset[&quot;refugees&quot;],
                mode=&quot;lines&quot;,
                name=coa_name,
                showlegend=False,
                line=dict(
                    color=df_subset[&quot;color&quot;].iloc[0], dash=&quot;dot&quot;
                ),  # Use the first color from the subset
            )
        )


fig.update_layout(
    autosize=False, width=750, height=600, yaxis_title=&quot;Total Annual Refugee Arrivals&quot;
)
fig.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Where do most refugees from the country with the highest numbers go?&lt;/h1&gt;
&lt;p&gt;Next, we wanted to find out where Syrian refugees have found welcomes for their new homes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
fig = (
    df.query(&quot;coo_name == &apos;Syrian Arab Rep.&apos;&quot;)
    .groupby(&quot;coa_name&quot;)
    .refugees.sum()
    .pipe(lambda x: x / x.sum())
    .mul(100)
    .sort_values(ascending=False)
    .head(10)
    .to_frame()
    .reset_index()
    .pipe(
        lambda df: px.bar(
            df,
            y=&quot;coa_name&quot;,
            x=&quot;refugees&quot;,
            labels={
                &quot;refugees&quot;: &quot;Acceptance Rate of Syrian Refugees (%)&quot;,
                &quot;coa_name&quot;: &quot;&quot;,
            },
            height=500,
            width=1000,
        )
    )
)
fig.update_yaxes(tickangle=0, automargin=True)
fig
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Which countries have the highest numbers of internally displaced persons?&lt;/h1&gt;
&lt;p&gt;Next, we&apos;ll explore the count of internally displaced persons (IDPs). An IDP is someone who has had to leave their home or usual residence because of reasons like armed conflict, violence, human rights violations, natural disasters, or humanitarian emergencies. Unlike refugees, IDPs haven&apos;t crossed international borders to find safety; they stay within their own country&apos;s borders&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;

fig = (
    df.groupby([&quot;coo_iso&quot;, &quot;coo_name&quot;])
    .idps.sum()
    .sort_values(ascending=False)
    .to_frame()
    .reset_index()
    .head(50)
    .pipe(
        lambda df: px.scatter_geo(
            df,
            locations=&quot;coo_iso&quot;,
            geojson=countries,
            hover_name=&quot;coo_name&quot;,
            # color=&apos;coo_name&apos;,
            featureidkey=&quot;properties.iso_a3&quot;,
            size=&quot;idps&quot;,
            # opacity = 0.6,
            width=1200,
            height=700,
        )
    )
)
fig
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;What countries have received the highest stateless populations?&lt;/h1&gt;
&lt;p&gt;Next, we focus on countries that have provided refuge to a substantial number of stateless individuals. Stateless persons are individuals who don&apos;t have the legal status of citizenship in any country. They lack the rights and protection typically granted to citizens. Stateless people often encounter significant difficulties in accessing education, healthcare, jobs, and the freedom to travel.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;

fig = (
    df.groupby([&quot;coa_iso&quot;, &quot;coa_name&quot;])
    .stateless.sum()
    .pipe(lambda x: x / 49036122)
    .sort_values(ascending=False)
    .mul(100)
    .to_frame()
    .reset_index()
    .head(10)
    .pipe(
        lambda df: px.scatter(
            df,
            y=&quot;coa_name&quot;,
            x=&quot;stateless&quot;,
            labels={
                &quot;stateless&quot;: &quot;Acceptance Rate of Stateless Populations (%)&quot;,
                &quot;coa_name&quot;: &quot;&quot;,
            },
            height=500,
            width=800,
        )
    )
)
fig.update_yaxes(tickangle=0, automargin=True)
fig.update_layout(xaxis_range=[0, 100])

fig
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And there you have it, everyone. That wraps up this week&apos;s data exploration. I&apos;m cant&apos;t wait for what Tidy Tuesday has in store for us next week. Until then, take good care of yourselves, and I&apos;ll catch you soon!&lt;/p&gt;
</content:encoded><author>Adam Cseresznye</author></item><item><title>Mastering Pandas DataFrame Styling for a Stunning Presentation</title><link>https://adamcseresznye.github.io/posts/pandas-styler</link><guid isPermaLink="true">https://adamcseresznye.github.io/posts/pandas-styler</guid><pubDate>Sun, 27 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1529078155058-5d716f45d604?ixlib=rb-4.0.3&amp;amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=2069&amp;amp;q=80&quot; alt=&quot;Image by Mika Baumeister&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As you wrap up your data analysis journey, you face a fun decision: how to share your discoveries effectively. Tables and graphs both have their moments to shine. Tables work great when you want your audience to spot exact values and compare them. But, here&apos;s the twist: tables can look overwhelming at first, especially if they&apos;re crammed with data.&lt;/p&gt;
&lt;p&gt;But don&apos;t worry! Styling and formatting are here to help. Pandas comes to the rescue. It lets you turn your data into stylish tables effortlessly.&lt;/p&gt;
&lt;p&gt;In this article, we&apos;ll explore some tricks to make your Pandas DataFrames look awesome and tell a clear data story. For the demonstrations, we&apos;ll dive into Seaborn&apos;s built-in &quot;tips&quot; dataset. This dataset is a nifty data frame with 244 rows and 7 variables, offering a peek into the world of tipping. This comprehensive collection includes variables like the tip amount in dollars, the bill total in dollars, the gender of the bill payer, the presence of smokers in the party, the day of the week, the time of day, and the party size. Ready to roll? Let&apos;s jump right in!&lt;/p&gt;
&lt;h1&gt;Setup&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;import pandas as pd
import numpy as np
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
df = pd.read_csv(
    &quot;https://raw.githubusercontent.com/mwaskom/seaborn-data/master/tips.csv&quot;
)
df.sample(5)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Built-in styling&lt;/h1&gt;
&lt;p&gt;In this article, we won&apos;t cover every single feature of the &lt;a href=&quot;https://pandas.pydata.org/docs/user_guide/style.html#Table-Visualization&quot;&gt;Styler class&lt;/a&gt; because that might make it too long. Instead, we&apos;ll concentrate on the practical aspects that can simplify your data analysis, improve data comprehension, and make your message clearer to your audience. So, instead of taking a deep dive, we&apos;ll start with the low-hanging fruits and explore the built-in options that Pandas offers.&lt;/p&gt;
&lt;p&gt;Before we dive in, let&apos;s prepare an aggregated dataframe for our demonstrations. Here it is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
grouped = df.groupby([&quot;day&quot;, &quot;smoker&quot;, &quot;time&quot;]).median(numeric_only=True)
grouped
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Highlighting minimum and maximum values&lt;/h2&gt;
&lt;p&gt;Check out these handy helpers: &lt;code&gt;highlight_min&lt;/code&gt; and &lt;code&gt;highlight_max&lt;/code&gt;. They&apos;re your go-to for spotting the lowest and highest values in each column. It&apos;s a speedy way to emphasize the most important data points in each category.&lt;/p&gt;
&lt;p&gt;Also, please note the &lt;code&gt;format(precision=1, thousands=&quot;,&quot;, decimal=&quot;.&quot;)&lt;/code&gt; snippet, this is not exactly a built-in style feature but has everything to do with keeping those float numbers tidy. Pandas tends to display more decimal places than we often require, which can be a bit distracting. To tone it down a notch, we can lean on the &lt;code&gt;format()&lt;/code&gt; and &lt;code&gt;format_index()&lt;/code&gt; methods to fine-tune the precision. Trust me, it&apos;s super useful!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    grouped.style.format(precision=1, thousands=&quot;,&quot;, decimal=&quot;.&quot;)
    .highlight_max(
        axis=0, props=&quot;color:white; font-weight:bold; background-color:green;&quot;
    )
    .highlight_min(axis=0, props=&quot;color:white; font-weight:bold; background-color:red;&quot;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Emphasizing Values Within a Range&lt;/h2&gt;
&lt;p&gt;Imagine you&apos;re want to find the days when tips fell between 3 and 5 dollars. In this scenario, the &lt;code&gt;highlight_between&lt;/code&gt; method comes to the rescue. Don&apos;t forget to use the &lt;code&gt;subset&lt;/code&gt; argument; it&apos;s your trusty sidekick when you only want to work with selected columns.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    grouped.reset_index()
    .style.format(precision=1, thousands=&quot;,&quot;, decimal=&quot;.&quot;)
    .highlight_between(
        left=3,
        right=5,
        subset=[&quot;tip&quot;],
        axis=1,
        props=&quot;color:white; font-weight:bold; background-color:purple;&quot;,
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Highlight Column-Wise Outliers&lt;/h2&gt;
&lt;p&gt;Utilizing parameters such as &lt;code&gt;q_left=0.05&lt;/code&gt;, &lt;code&gt;q_right=0.95&lt;/code&gt;, &lt;code&gt;axis=0&lt;/code&gt;, and defining &lt;code&gt;props=&apos;opacity: 10%;&apos;&lt;/code&gt;, we can highlight values residing outside the 5-95 percentile range.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    grouped.style.format(precision=1, thousands=&quot;,&quot;, decimal=&quot;.&quot;).highlight_quantile(
        q_left=0.05, q_right=0.95, axis=0, props=&quot;opacity: 10%;&quot;
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Spot trends Using Color Gradients&lt;/h2&gt;
&lt;p&gt;We can show data trends using both the &lt;code&gt;background_gradient&lt;/code&gt; and &lt;code&gt;text_gradient&lt;/code&gt; methods. These methods introduce gradient-style background colors and text shading to our visualizations. To optimize their impact, it&apos;s advisable to first arrange your data with the &lt;code&gt;sort_values&lt;/code&gt; method before applying the &lt;code&gt;background_gradient&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    grouped.reset_index()
    .sort_values(by=&quot;total_bill&quot;)
    .style.format(precision=1, thousands=&quot;,&quot;, decimal=&quot;.&quot;)
    .background_gradient(cmap=&quot;viridis&quot;, axis=0)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
(
    grouped.reset_index()
    .sort_values(by=&quot;total_bill&quot;)
    .style.format(precision=1, thousands=&quot;,&quot;, decimal=&quot;.&quot;)
    .text_gradient(cmap=&quot;bwr&quot;, axis=0)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Display Bar Charts within Your Table&lt;/h2&gt;
&lt;p&gt;Let&apos;s explore a technique for highlighting the significance of values by embedding bar charts right within the cells. The blend of bar heights and color gradients can pack a powerful punch in your data storytelling arsenal. Don&apos;t forget to experiment with the &apos;align&apos; option, a handy tool that helps you position these bars within the cells just right, giving your visuals a polished look. Feel free to play around with the settings and find what clicks best with your data tales.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    grouped.reset_index()
    .sort_values(by=&quot;total_bill&quot;)
    .style.format(precision=1, thousands=&quot;,&quot;, decimal=&quot;.&quot;)
    .bar(
        align=&quot;mean&quot;,
        cmap=&quot;bwr&quot;,
        height=50,
        width=60,
        props=&quot;width: 120px; border-right: 1px solid black;&quot;,
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
(
    grouped.reset_index()
    .sort_values(by=&quot;total_bill&quot;)
    .style.format(precision=1, thousands=&quot;,&quot;, decimal=&quot;.&quot;)
    .bar(
        align=0,
        cmap=&quot;bwr&quot;,
        height=50,
        width=60,
        props=&quot;width: 120px; border-right: 1px solid black;&quot;,
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Formatting&lt;/h2&gt;
&lt;p&gt;Below is an illustrative example of the myriad possibilities when it comes to fine-tuning the style of a DataFrame. In this code, we&apos;ve undertaken various stylistic enhancements:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Background Gradient&lt;/strong&gt;: We&apos;ve applied a nice background color gradient to the columns along the vertical axis.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caption&lt;/strong&gt;: We&apos;ve set a descriptive caption for the DataFrame.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visual Styling&lt;/strong&gt;: We&apos;ve specified text alignment and introduced hovering options to make highlighted values pop even more.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Column Names Formatting&lt;/strong&gt;: We&apos;ve reformatted our column names for clarity and aesthetics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Column Hiding&lt;/strong&gt;: We&apos;ve concealed the &apos;smoker&apos; column, decluttering our view.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Index Hiding&lt;/strong&gt;: We&apos;ve also hidden the index labels for a cleaner look.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Numerical Formatting&lt;/strong&gt;: We&apos;ve individually specified the numerical representation, including adding a dollar sign for currency values.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This demonstration underscores the virtually endless possibilities for customizing the appearance of your DataFrame. However, it&apos;s important to note that the physical attributes set by the &lt;code&gt;set_table_styles&lt;/code&gt; method won&apos;t be exported to Excel, should you choose to do so. Just a handy tidbit to keep in mind.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;

# Start by resetting the index and renaming columns with underscores
# Replace underscores with spaces for better readability
(
    grouped.reset_index().rename(columns=lambda x: x.replace(&quot;_&quot;, &quot; &quot;))
    # Sort the DataFrame by the &apos;total bill&apos; column
    .sort_values(by=&quot;total bill&quot;)
    # Apply Pandas Styler to format the table
    .style
    # Apply background color gradient to columns along the vertical axis (axis=0)
    .background_gradient(cmap=&quot;viridis&quot;, axis=0)
    # Set a caption for the table
    .set_caption(&quot;Exploring Dining Trends: Bill Amounts, Tips, and Party Sizes&quot;)
    # Customize the table&apos;s visual styling
    .set_table_styles(
        [
            {
                &quot;selector&quot;: &quot;th.col_heading&quot;,
                &quot;props&quot;: &quot;text-align: center; font-size: 1.5em;&quot;,
            },
            {&quot;selector&quot;: &quot;td&quot;, &quot;props&quot;: &quot;text-align: center;&quot;},
            {
                &quot;selector&quot;: &quot;td:hover&quot;,
                &quot;props&quot;: &quot;font-style: italic; color: black; font-weight:bold; background-color : #ffffb3;&quot;,
            },
        ],
        overwrite=False,
    )
    # Apply custom formatting to the index labels (convert to uppercase)
    .format_index(str.upper, axis=1)
    # Hide the &apos;smoker&apos; column from the table
    .hide(subset=[&quot;smoker&quot;], axis=1)
    # Hide the index label (row numbers)
    .hide(axis=&quot;index&quot;)
    # Format specific columns with dollar signs and one decimal place
    .format(
        {
            &quot;total bill&quot;: &quot;$ {:.1f}&quot;,
            &quot;tip&quot;: &quot;$ {:.1f}&quot;,
            &quot;size&quot;: &quot;{:.0f}&quot;,
        }
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Defining Custom Styling&lt;/h1&gt;
&lt;p&gt;If the predefined styling methods don&apos;t meet your needs, you have the flexibility to create your custom styling function and use it with your DataFrame. You can apply styling element by element using the &lt;code&gt;.applymap()&lt;/code&gt; method, or you can work on columns or rows using the &lt;code&gt;.apply()&lt;/code&gt; method. If you need to work specifically on column header rows or indexes, both &lt;code&gt;.apply_index()&lt;/code&gt; and &lt;code&gt;.applymap_index()&lt;/code&gt; are at your disposal.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
# Define a custom styling function that highlights &apos;Sun&apos; in yellow
def highlight_sunday(value, color):
    return f&quot;color: {color};&quot; if value == &quot;Sun&quot; else None


# Sample 10 random rows from the DataFrame
(
    df.sample(10).style
    # Set formatting options for numeric values
    .format(precision=1, thousands=&quot;,&quot;, decimal=&quot;.&quot;)
    # Apply the highlight_sunday function to the &apos;day&apos; column and set the color to yellow
    .applymap(highlight_sunday, color=&quot;yellow&quot;, subset=[&quot;day&quot;])
    # Apply opacity styling to cells in the &apos;size&apos; column where the value is less than or equal to 3
    .applymap(lambda value: &quot;opacity: 50%;&quot; if (value &amp;lt;= 3) else None, subset=[&quot;size&quot;])
    # Apply red text color styling to cells in the &apos;tip&apos; column where the value is less than or equal to 3
    .applymap(lambda value: &quot;color:red;&quot; if (value &amp;lt;= 3) else None, subset=[&quot;tip&quot;])
    # Use apply_index to style index labels. If the label is &apos;tip&apos;, &apos;day&apos;, or &apos;size&apos;, set the color to white, otherwise black.
    .apply_index(
        lambda s: np.where(
            s.isin([&quot;tip&quot;, &quot;day&quot;, &quot;size&quot;]), &quot;opacity: 100%;&quot;, &quot;opacity: 10%;&quot;
        ),
        axis=1,
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Other Useful Features&lt;/h1&gt;
&lt;h2&gt;Get a Clearer Overview with &lt;code&gt;set_sticky&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;What if you find yourself facing a DataFrame with more columns than can comfortably fit on your screen, yet you still wish to inspect each column individually? In the past, you might have resorted to using &lt;code&gt;pd.set_option(&apos;display.max_columns&apos;, xyz)&lt;/code&gt; to expand the display. However, there&apos;s a much more elegant solution: &lt;code&gt;set_sticky&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;set_sticky&lt;/code&gt; introduces a clever CSS trick that permanently pins the index or column headers within a scrolling frame. In our case, although the &apos;tips&apos; DataFrame doesn&apos;t have an excessive number of columns, we&apos;ve  concatenated 10 DataFrames together to showcase the remarkable utility of &lt;code&gt;set_sticky&lt;/code&gt;. As you scroll horizontally, you&apos;ll notice that you can now conveniently inspect all the columns while the index remains firmly in place, thanks to the magic of &lt;code&gt;set_sticky&lt;/code&gt;. Let&apos;s explore this feature below.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(pd.concat([df for i in range(10)], axis=1).head().style.set_sticky(axis=&quot;index&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Concatenating DataFrame Outputs&lt;/h2&gt;
&lt;p&gt;You can combine two or more Stylers if they have the same columns. This is particularly handy when presenting summary statistics for a DataFrame&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    df.groupby(&quot;day&quot;)[[&quot;total_bill&quot;, &quot;tip&quot;, &quot;size&quot;]]
    .mean()
    .style.format(precision=1)
    .concat(
        df[[&quot;total_bill&quot;, &quot;tip&quot;, &quot;size&quot;]]
        .agg([&quot;mean&quot;, &quot;median&quot;, &quot;sum&quot;])
        .style.format(precision=1)
        .relabel_index([&quot;Average&quot;, &quot;Median&quot;, &quot;Sum&quot;])
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Exporting&lt;/h1&gt;
&lt;p&gt;To save your styled DataFrame in Excel format, you can export it to an .xlsx file, preserving the styling. To do this, ensure you have the &apos;openpyxl&apos; package installed.&lt;/p&gt;
&lt;p&gt;And there you have it, folks! This concludes our brief exploration of the formatting options available right within Pandas. I hope you found this article insightful and enjoyable. Until next time, happy coding!&lt;/p&gt;
</content:encoded><author>Adam Cseresznye</author></item><item><title>Tidy Tuesday: Union Membership in the United States</title><link>https://adamcseresznye.github.io/posts/labour-day</link><guid isPermaLink="true">https://adamcseresznye.github.io/posts/labour-day</guid><pubDate>Tue, 05 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1572371402638-c654219dd0f8?ixlib=rb-4.0.3&amp;amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=2070&amp;amp;q=80&quot; alt=&quot;Photo by Claudio Schwarz&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Happy Labor Day! 🛠️ As we celebrate the achievements of workers and the contributions they&apos;ve made to society, what better way to delve into the world of labor and employment than with a fresh &lt;a href=&quot;https://github.com/rfordatascience/tidytuesday/blob/master/data/2023/2023-09-05/readme.md&quot;&gt;Tidy Tuesday dataset&lt;/a&gt;? This week&apos;s data comes from the comprehensive &quot;Union Membership, Coverage, and Earnings from the CPS&quot; dataset, courtesy of Barry Hirsch from Georgia State University, David Macpherson from Trinity University, and William Even from Miami University.&lt;/p&gt;
&lt;p&gt;This dataset is a rich source of information, shedding light on the intricate dynamics of union membership, coverage, and earnings. So, grab your coffee, get comfortable, and let&apos;s embark on this Tidy Tuesday journey together, exploring the intricate tapestry of labor data!&lt;/p&gt;
&lt;h1&gt;Import data&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from scipy import stats
import geopandas
from lets_plot.geo_data import *

from lets_plot import *
from lets_plot.mapping import as_discrete

LetsPlot.setup_html()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are three distinct datasets to investigate: one focusing on demographic information, another centered on wage data, and the third associated with various states. Let&apos;s read them all in.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;demographic_df = pd.read_csv(
    &quot;https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2023/2023-09-05/demographics.csv&quot;
)
wages_df = pd.read_csv(
    &quot;https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2023/2023-09-05/wages.csv&quot;
)
states_df = pd.read_csv(
    &quot;https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2023/2023-09-05/states.csv&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Data dictionaries&lt;/h1&gt;
&lt;p&gt;Here are the corresponding data dictionaries:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demographics data:&lt;/strong&gt;
Data sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1973-1981: May Current Population Survey (CPS)&lt;/li&gt;
&lt;li&gt;1982: No union questions available&lt;/li&gt;
&lt;li&gt;1983-2022: CPS Outgoing Rotation Group (ORG) Earnings Files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The definition of union membership was expanded in 1977 to include &quot;employee associations similar to a union&quot;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;variable&lt;/th&gt;
&lt;th&gt;class&lt;/th&gt;
&lt;th&gt;description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;year&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;When the data was collected.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sample_size&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;The number of wage and salary workers ages 16 and over who were surveyed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;employment&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Wage and salary employment in thousands.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;members&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Employed workers who are union members in thousands.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;covered&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Workers covered by a collective bargaining agreement in thousands.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p_members&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Percent of employed workers who are union members.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p_covered&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Percent of employed workers who are covered by a collective bargaining agreement.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;facet&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;The sector or demographic group contained in this row of data.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Wages data:&lt;/strong&gt;
Data sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1973-1981: May Current Population Survey (CPS)&lt;/li&gt;
&lt;li&gt;1982: No union questions available&lt;/li&gt;
&lt;li&gt;1983-2022: CPS Outgoing Rotation Group (ORG) Earnings Files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The definition of union membership was expanded in 1977 to include &quot;employee associations similar to a union&quot;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;variable&lt;/th&gt;
&lt;th&gt;class&lt;/th&gt;
&lt;th&gt;description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;year&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;When the data was collected.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sample_size&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;The number of wage and salary workers ages 16 and over who were surveyed and provided earnings and hours worked information.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wage&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Mean hourly earnings in nominal dollars.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;at_cap&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Percent of workers with weekly earnings at the top code of $999 through 1988, $1923 in 1989-97, and $2885 beginning in 1998, with individuals assigned mean earnings above the cap based on annual estimates of the gender-specific Pareto distribution.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;union_wage&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Mean wage among union members.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nonunion_wage&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Mean wage among nonunion workers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;union_wage_premium_raw&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;The percentage difference between the union and nonunion wage.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;union_wage_premium_adjusted&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Estimated as exp(b)-1 where b is the regression coefficient on a union membership variable (equal to 1 if union and 0 otherwise) from a semi-logarithmic wage equation, with controls included for worker/job characteristics. Included in the all-worker wage equation are the control variables: years of schooling, potential years of experience [proxied by age minus years of schooling minus 6] and its square [both interacted with gender], and categorical variables for marital status, race and ethnicity, gender, part-time, large metropolitan area, state, public sector, broad industry, and broad occupation. Controls are omitted, as appropriate, for estimates within sectors or by demographic group [i.e., by class, gender, race, or industry sector]. Workers who do not report earnings but instead have them imputed [i.e., assigned] by the Census are removed from the estimation samples in all years, except 1994 and 1995 when imputed earners cannot be identified. Inclusion of imputed earners causes union wages to be understated, nonunion wages overstated, and union-nonunion wage differences understated. For 1994-95, the sample includes imputed earners and estimates in those years have been adjusted to remove the bias from imputation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;facet&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;The sector or demographic group contained in this row of data.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;States data:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Data source: Current Population Survey (CPS) Outgoing Rotation Group (ORG) Earnings Files&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;variable&lt;/th&gt;
&lt;th&gt;class&lt;/th&gt;
&lt;th&gt;description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;state_census_code&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Census state code used in CPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;state&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;State name.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sector&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Employment sector.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;observations&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;CPS sample size.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;employment&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Wage and salary employment in thousands.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;members&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Employed workers who are union members in thousands.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;covered&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Workers covered by a collective bargaining agreement in thousands.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p_members&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Percent of employed workers who are union members.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p_covered&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Percent of employed workers who are covered by a collective bargaining agreement.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;state_abbreviation&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;State abbreviation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;year&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Year of the survey.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;As in our previous Tidy Tuesday blog, I believe we can address the following questions using this dataset:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What are the overarching trends in the labor force, particularly regarding union memberships?&lt;/li&gt;
&lt;li&gt;Do specific demographic groups or occupations display a higher likelihood of union membership?&lt;/li&gt;
&lt;li&gt;Do union members experience any financial advantages or benefits compared to non-union workers?&lt;/li&gt;
&lt;li&gt;Which states have the highest number of union members or affiliated unions?&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;What are the overarching trends in the labor force, particularly regarding union memberships?&lt;/h1&gt;
&lt;p&gt;The figure below illustrates a consistent upward trajectory in the workforce&apos;s growth, whereas union membership and the number of individuals covered by unions have been on a declining trend over the decades.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    demographic_df.query(&quot;facet.str.contains(&apos;all wage&apos;)&quot;)
    .drop(columns=[&quot;sample_size&quot;, &quot;facet&quot;, &quot;p_members&quot;, &quot;p_covered&quot;])
    .melt(id_vars=&quot;year&quot;, var_name=&quot;Description&quot;)
    .pipe(
        lambda df: ggplot(df)
        + geom_line(aes(&quot;year&quot;, &quot;value&quot;, color=&quot;Description&quot;), size=1.5)
        + labs(
            title=&quot;Evolution of Workforce Size and Union Membership Across the Years&quot;,
            subtitle=&quot;While employment numbers have been on the rise, the proportion of employed workers \nwho are union members and covered by unions has been steadily decreasing.&quot;,
            x=&quot;&quot;,
            y=&quot;Number of people in thousands&quot;,
            caption=&quot;© 2023 by Barry T. Hirsch, David A. Macpherson, and William E. Even.&quot;,
        )
        + theme(
            plot_subtitle=element_text(size=12, face=&quot;italic&quot;),
            plot_title=element_text(size=15, face=&quot;bold&quot;),
        )
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Do specific demographic groups or occupations display a higher likelihood of union membership?&lt;/h1&gt;
&lt;p&gt;Next, our investigation will focus on identifying potential demographic disparities among workforce members and uncovering the professions with the highest likelihood of union membership. We&apos;ll utilize the &lt;em&gt;&quot;p_members&quot;&lt;/em&gt; column, which represents the percentage of employed workers who are union members, to delve into these aspects.&lt;/p&gt;
&lt;p&gt;As evident in @fig-fig2, a substantial decrease in union membership is observable across nearly all demographic groups over the decades.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    demographic_df.query(&quot;facet.str.contains(&apos;demographics&apos;)&quot;)
    .assign(facet=lambda df: df.facet.replace({&quot;demographics:&quot;: &quot;&quot;}, regex=True))
    .pipe(
        lambda df: ggplot(df, aes(&quot;year&quot;, &quot;p_members&quot;, color=&quot;facet&quot;))
        + geom_line(
            size=1.5,
        )
        + labs(
            title=&quot;Demographic Trends in Union Membership as a Percentage of the Total Workforce Over Time&quot;,
            subtitle=&quot;Almost all demographic groups exhibit a significant decline in union membership over the decades.&quot;,
            x=&quot;&quot;,
            y=&quot;Percent of employed workers who are union members&quot;,
            caption=&quot;© 2023 by Barry T. Hirsch, David A. Macpherson, and William E. Even.&quot;,
        )
        + theme(
            plot_subtitle=element_text(size=12, face=&quot;italic&quot;),
            plot_title=element_text(size=15, face=&quot;bold&quot;),
        )
        + ggsize(800, 500)
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While certain professions exhibit declining trends in union memberships, it&apos;s noteworthy that some public sector occupations, such as postal service, police, and local government jobs, maintain the highest and most consistent levels of union participation (@fig-fig3).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    demographic_df.query(
        &quot;(~facet.str.contains(&apos;demographics&apos;)) and (~facet.str.contains(&apos;all&apos;))&quot;
    ).pipe(
        lambda df: ggplot(df, aes(&quot;year&quot;, &quot;p_members&quot;, color=&quot;facet&quot;))
        + geom_line(
            size=1.5,
        )
        + labs(
            title=&quot;Workforce Sector-Specific Trends in Union Membership&quot;,
            subtitle=&quot;Public sector occupations demonstrate higher union participation rates.&quot;,
            x=&quot;&quot;,
            y=&quot;Percent of employed workers who are union members&quot;,
            caption=&quot;© 2023 by Barry T. Hirsch, David A. Macpherson, and William E. Even.&quot;,
        )
        + theme(
            plot_subtitle=element_text(size=12, face=&quot;italic&quot;),
            plot_title=element_text(size=15, face=&quot;bold&quot;),
        )
        + ggsize(800, 500)
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Do union members experience any financial advantages or benefits compared to non-union workers?&lt;/h1&gt;
&lt;p&gt;The next figure is both straightforward and highly impactful, providing a direct comparison of average wages for union members versus non-union members.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    wages_df.query(&quot;facet == &apos;all wage and salary workers&apos;&quot;)
    .drop(
        columns=[
            &quot;wage&quot;,
            &quot;sample_size&quot;,
            &quot;at_cap&quot;,
            &quot;union_wage_premium_raw&quot;,
            &quot;union_wage_premium_adjusted&quot;,
            &quot;facet&quot;,
        ]
    )
    .melt(id_vars=&quot;year&quot;, var_name=&quot;Description&quot;)
    .pipe(
        lambda df: ggplot(df, aes(&quot;year&quot;, &quot;value&quot;, color=&quot;Description&quot;))
        + geom_line(
            size=1.5,
        )
        + labs(
            title=&quot;Differences in Mean Hourly Earnings for Union and Non-Union Workers&quot;,
            subtitle=&quot;Union workers consistently display higher mean wages in comparison to non-union members.&quot;,
            x=&quot;&quot;,
            y=&quot;Mean hourly earnings in nominal dollars&quot;,
            caption=&quot;© 2023 by Barry T. Hirsch, David A. Macpherson, and William E. Even.&quot;,
        )
        + theme(
            plot_subtitle=element_text(size=12, face=&quot;italic&quot;),
            plot_title=element_text(size=15, face=&quot;bold&quot;),
        )
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As with our previous figures, we will now delve into demographic variations and job sector disparities. To unveil these potential distinctions, we will use the &lt;em&gt;&quot;union_wage_premium_raw&quot;&lt;/em&gt; column, which indicates the percentage difference between union and non-union wages.&lt;/p&gt;
&lt;p&gt;The most significant disparities in hourly wages (@fig-fig5) between unionized and non-unionized workers are observed within the construction sector. These disparities have fluctuated over the decades, ranging from below 40% to as high as 80%. It&apos;s important to note that almost all sectors exhibit higher earnings for union members, except for federal, manufacturing, and wholesale/retail workers (particularly in recent times).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    wages_df.query(
        &quot;(~facet.str.contains(&apos;demographics&apos;)) and (~facet.str.contains(&apos;all&apos;))&quot;
    ).pipe(
        lambda df: ggplot(df, aes(&quot;year&quot;, &quot;union_wage_premium_raw&quot;, color=&quot;facet&quot;))
        + geom_line(
            size=1.5,
        )
        + labs(
            title=&quot;Sector-specific Differences in Mean Hourly Earnings for Union and Non-Union Workers&quot;,
            subtitle=&quot;The disparities between wages range from -20% to +80%.&quot;,
            x=&quot;&quot;,
            y=&quot;The percentage difference between the union and nonunion wage&quot;,
            caption=&quot;© 2023 by Barry T. Hirsch, David A. Macpherson, and William E. Even.&quot;,
        )
        + theme(
            plot_subtitle=element_text(size=12, face=&quot;italic&quot;),
            plot_title=element_text(size=15, face=&quot;bold&quot;),
        )
        + ggsize(800, 500)
        + geom_hline(yintercept=0, linetype=5, color=&quot;#6c6c6c&quot;, size=1)
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In addition to gender-specific differences, it&apos;s intriguing to observe the relationship between education levels and wage disparities between unionized and non-unionized workers. Workers with education levels below college tend to benefit more from union membership, whereas, in general, individuals with higher education levels tend to have lower wages when affiliated with a union.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    wages_df.query(&quot;facet.str.contains(&apos;demographics&apos;)&quot;)
    .assign(facet=lambda df: df.facet.replace({&quot;demographics:&quot;: &quot;&quot;}, regex=True))
    .pipe(
        lambda df: ggplot(df, aes(&quot;year&quot;, &quot;union_wage_premium_raw&quot;, color=&quot;facet&quot;))
        + geom_line(
            size=1.5,
        )
        + labs(
            title=&quot;Demographics-specific Differences in Mean Hourly Earnings for Union and Non-Union Workers&quot;,
            subtitle=&quot;It appears that females tend to benefit more from union membership in comparison to males, \nparticularly in terms of the percentage difference between union and non-union wages.&quot;,
            x=&quot;&quot;,
            y=&quot;The percentage difference between the union and nonunion wage&quot;,
            caption=&quot;© 2023 by Barry T. Hirsch, David A. Macpherson, and William E. Even.&quot;,
        )
        + theme(
            plot_subtitle=element_text(size=12, face=&quot;italic&quot;),
            plot_title=element_text(size=15, face=&quot;bold&quot;),
        )
        + ggsize(800, 500)
        + geom_hline(yintercept=0, linetype=5, color=&quot;#6c6c6c&quot;, size=1)
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Which states have the highest number of union members or affiliated unions?&lt;/h1&gt;
&lt;p&gt;To illustrate variations in union membership across states, we will leverage Lets Plot&apos;s capabilities for visualizing spatial data. Given the extensive dataset, our focus will be on highlighting a subset of the data, specifically the most recent trends in 2022 across all sectors.&lt;/p&gt;
&lt;p&gt;As evident in @fig-fig7, certain states like New York, Alaska, Hawaii, Washington, California, and Oregon have a significant portion of their workforce as union members. Conversely, states like Utah, South Dakota, and the Carolinas display lower percentages of union membership among their workforce.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
filtered_data = states_df.query(&quot;sector == &apos;Total&apos; and year == 2022&quot;)
boundaries = (
    geocode_states(filtered_data[&quot;state&quot;])
    .scope(&quot;USA&quot;)
    .inc_res(4)
    .get_boundaries(resolution=4)
)

(
    ggplot()
    + geom_livemap()
    + geom_polygon(
        aes(color=&quot;p_members&quot;, fill=&quot;p_members&quot;),
        data=states_df.query(&quot;sector == &apos;Total&apos; and year == 2022&quot;),
        map=boundaries,
        alpha=0.9,
        map_join=[[&quot;state&quot;], [&quot;state&quot;]],
        show_legend=False,
        size=0.01,
    )
    + theme(
        axis_title=&quot;blank&quot;, axis_text=&quot;blank&quot;, axis_ticks=&quot;blank&quot;, axis_line=&quot;blank&quot;
    )
    + scale_fill_brewer(palette=&quot;Greens&quot;)
    + ggsize(800, 500)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There you have it! I hope you found this week&apos;s Tidy Tuesday analysis insightful. I would encourage everyone to dive deeper into this dataset as I&apos;ve only scratched the surface; there&apos;s a wealth of knowledge waiting to be uncovered here.&lt;/p&gt;
&lt;p&gt;Happy coding, and until next time, see you in our next exploration!&lt;/p&gt;
</content:encoded><author>Adam Cseresznye</author></item><item><title>Tidy Tuesday: The Global Human Day</title><link>https://adamcseresznye.github.io/posts/human-day</link><guid isPermaLink="true">https://adamcseresznye.github.io/posts/human-day</guid><pubDate>Mon, 11 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1480480565647-1c4385c7c0bf?ixlib=rb-4.0.3&amp;amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=1931&amp;amp;q=80&quot; alt=&quot;Photo by Filip Mroz&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This week&apos;s &lt;a href=&quot;https://github.com/rfordatascience/tidytuesday/blob/master/data/2023/2023-09-12/readme.md&quot;&gt;Tidy Tuesday&lt;/a&gt; takes us to &lt;a href=&quot;https://www.pnas.org/doi/10.1073/pnas.2219564120&quot;&gt;The Human Chronome Project&lt;/a&gt;, an initiative based at McGill University in Montreal. We&apos;re exploring insights from their paper titled &quot;The global human day in PNAS&quot; and the accompanying dataset on &lt;a href=&quot;https://zenodo.org/record/8040631&quot;&gt;Zenodo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The publication offers a broad view of our species&apos; activities, shedding light on how economic activities fit into the grand scheme of life. It also highlights activities with significant potential for change. For a deeper dive into our research methodology and to explore supplementary visualizations, I encourage you to peruse the supplementary materials provided with the publication.&lt;/p&gt;
&lt;h1&gt;Import data&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from scipy import stats
import statsmodels.formula.api as smf
import statsmodels.api as sm
from lets_plot.bistro.qq import qq_plot

import flag
from PIL import Image
from pathlib import Path

from lets_plot import *
from lets_plot.mapping import as_discrete

LetsPlot.setup_html()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While we have four distinct datasets at our disposal, our primary focus will be on two of them. The &apos;all_countries&apos; dataset holds comprehensive information about the time allocated to each activity per region and country, while the &apos;country_regions&apos; dataset provides the essential correspondence between country names and their ISO-2 and ISO-3 abbreviations. Let&apos;s proceed by reading them all in.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;all_countries = pd.read_csv(
    &quot;https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2023/2023-09-12/all_countries.csv&quot;
)
country_regions = pd.read_csv(
    &quot;https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2023/2023-09-12/country_regions.csv&quot;
)
global_human_day = pd.read_csv(
    &quot;https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2023/2023-09-12/global_human_day.csv&quot;
)
global_economic_activity = pd.read_csv(
    &quot;https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2023/2023-09-12/global_economic_activity.csv&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Data dictionaries&lt;/h1&gt;
&lt;p&gt;Here are the corresponding data dictionaries:&lt;/p&gt;
&lt;h3&gt;Data Dictionary&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;all_countries.csv&lt;/code&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;variable&lt;/th&gt;
&lt;th&gt;class&lt;/th&gt;
&lt;th&gt;description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Category&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;M24 categories&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subcategory&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;M24 subcategories&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;country_iso3&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Country code in iso3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;region_code&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Region code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;population&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Population&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hoursPerDayCombined&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Hours per day combined for the country&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uncertaintyCombined&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Uncertainty combined. Uncertainty is in units variance.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;country_regions.csv&lt;/code&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;variable&lt;/th&gt;
&lt;th&gt;class&lt;/th&gt;
&lt;th&gt;description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;region_code&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Region code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;region_name&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Region name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;country_name&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Country name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M49_code&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;M49 code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;country_iso2&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Country code in iso2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;country_iso3&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Country code in iso3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alt_country_name&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Alternative country name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alt_country_name1&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Alternative country name 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alt_country_name2&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Alternative country name 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alt_country_name3&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Alternative country name 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alt_country_name4&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Alternative country name 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alt_country_name5&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Alternative country name 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alt_country_name6&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Alternative country name 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;other_code1&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Other country code 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;other_code2&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;Other country code 2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;global_human_day.csv&lt;/code&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;variable&lt;/th&gt;
&lt;th&gt;class&lt;/th&gt;
&lt;th&gt;description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Subcategory&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;M24 subcategory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hoursPerDay&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Hours per day for all countries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uncertainty&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Uncertainty in units variance.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;global_economic_activity.csv&lt;/code&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;variable&lt;/th&gt;
&lt;th&gt;class&lt;/th&gt;
&lt;th&gt;description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Subcategory&lt;/td&gt;
&lt;td&gt;character&lt;/td&gt;
&lt;td&gt;M24 subcategory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hoursPerDay&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Hours per day for all countries.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uncertainty&lt;/td&gt;
&lt;td&gt;double&lt;/td&gt;
&lt;td&gt;Uncertainty in units variance.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;As in our previous Tidy Tuesday blog, I think we can address the following questions using this dataset:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Which regions allocate the most and least time to each subcategory?&lt;/li&gt;
&lt;li&gt;Which countries exhibit the highest and lowest time allocation for each subcategory?&lt;/li&gt;
&lt;li&gt;Is there a discernible relationship between time spent on each category and GDP per capita?&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Get GDP per capita data from Wikipedia&lt;/h1&gt;
&lt;p&gt;To prepare for our third question, let&apos;s grab &lt;a href=&quot;https://en.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)_per_capita&quot;&gt;GDP per capita&lt;/a&gt; data from Wikipedia. It will come in handy as we dive deeper into our analysis!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Define a dictionary for country name replacements
names_to_replace = {
    &quot;Russia&quot;: &quot;Russian Federation&quot;,
    &quot;South Korea&quot;: &quot;Korea, Republic of&quot;,
    &quot;Vietnam&quot;: &quot;Viet Nam&quot;,
    &quot;Hong Kong&quot;: &quot;Hong Kong, China&quot;,
    &quot;Iran&quot;: &quot;Iran, Islamic Republic of&quot;,
    &quot;Venezuela&quot;: &quot;Venezuela, Bolivarian Republic of&quot;,
    &quot;Tanzania&quot;: &quot;Tanzania, United Republic of&quot;,
    &quot;DR Congo&quot;: &quot;Congo&quot;,
    &quot;Palestine&quot;: &quot;Occupied Palestinian Territory&quot;,
    &quot;Brunei&quot;: &quot;Brunei Darussalam&quot;,
    &quot;North Korea&quot;: &quot;Korea Democratic People&apos;s Republic of&quot;,
    &quot;Moldova&quot;: &quot;Moldova, Republic of&quot;,
    &quot;Syria&quot;: &quot;Syrian Arab Republic&quot;,
    &quot;North Macedonia&quot;: &quot;Macedonia the former Yugoslav Republic of&quot;,
    &quot;Laos&quot;: &quot;Lao People&apos;s Democratic Republic&quot;,
    &quot;Eswatini&quot;: &quot;Swaziland&quot;,
    &quot;Curaçao&quot;: &quot;Curacao&quot;,
    &quot;East Timor&quot;: &quot;Timor-Leste&quot;,
    &quot;Saint Vincent and the Grenadines&quot;: &quot;Saint Vincent and Grenadines&quot;,
    &quot;Sint Maarten&quot;: &quot;Sint Maarten (Kingdom of the Netherlands)&quot;,
    &quot;São Tomé and Príncipe&quot;: &quot;Sao Tome and Principe&quot;,
    &quot;Micronesia&quot;: &quot;Micronesia Federated States of&quot;,
    &quot;Macau&quot;: &quot;Macau, China&quot;,
}

# Read GDP data from Wikipedia
gdp = (
    pd.read_html(&quot;https://en.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)&quot;)[2]
    .pipe(lambda s: s.set_axis(s.columns.map(&quot;_&quot;.join), axis=1))
    .loc[
        :,
        [
            &quot;Country/Territory_Country/Territory&quot;,
            &quot;UN region_UN region&quot;,
            &quot;United Nations[15]_Estimate&quot;,
        ],
    ]
    .rename(
        columns={
            &quot;Country/Territory_Country/Territory&quot;: &quot;country&quot;,
            &quot;UN region_UN region&quot;: &quot;UN_region&quot;,
            &quot;United Nations[15]_Estimate&quot;: &quot;gdp&quot;,
        }
    )
    .assign(country=lambda df: df.country.replace(names_to_replace))
    .assign(
        gdp=lambda df: df.gdp.replace(r&quot;\D+&quot;, &quot;&quot;, regex=True),
        iso3=lambda df: df.country.map(
            country_regions.loc[:, [&quot;alt_country_name&quot;, &quot;country_iso3&quot;]]
            .set_index(&quot;alt_country_name&quot;)
            .squeeze()
            .to_dict()
        ),
    )
    .query(&quot;~iso3.isna()&quot;)
)
gdp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After obtaining the GDP per capita data, we&apos;ll integrate it into our main dataset.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;df = (
    all_countries.assign(
        HPD_with_u=lambda df: df.hoursPerDayCombined + df.uncertaintyCombined
    )
    .assign(
        gdp=lambda df: df.country_iso3.map(gdp.set_index(&quot;iso3&quot;).gdp.to_dict()).astype(
            float
        )
    )
    .query(&quot;~gdp.isna()&quot;)
    .assign(
        gdp_per_population=lambda df: df.gdp / df.population.div(1_000_000),
        gdp_per_population_log=lambda df: np.log10(df.gdp_per_population),
        Subcategory=lambda df: df.Subcategory.str.replace(&quot; &quot;, &quot;_&quot;),
    )
)

df
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Which regions allocate the most and least time to each subcategory?&lt;/h1&gt;
&lt;p&gt;First, to provide an overview of the prevailing trends, we will begin by examining disparities in time allocation across various subcategories within different regions. Following this, we will delve into a more granular analysis (as part of our second question), considering the data at the country level. As depicted in @fig-fig1, a noticeable pattern emerges. Typically, the minimum and maximum values for each activity type appear quite similar. However, it gets intriguing when we focus on certain categories like &apos;Social,&apos; &apos;Food Growth &amp;amp; Collection,&apos; &apos;Schooling &amp;amp; Research,&apos; &apos;Sleep &amp;amp; Bedrest,&apos; and &apos;Passive.&apos; In these categories, we observe more significant differences, with gaps ranging from 1.05 to 1.59 hours between the maximum and minimum hours spent.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
maximum_values = (
    df.groupby([&quot;region_code&quot;, &quot;Subcategory&quot;])
    .mean(numeric_only=True)
    .reset_index()
    .groupby([&quot;Subcategory&quot;])
    .apply(lambda x: x.sort_values(ascending=False, by=&quot;hoursPerDayCombined&quot;).head(1))
    .reset_index(drop=True)[[&quot;region_code&quot;, &quot;Subcategory&quot;, &quot;hoursPerDayCombined&quot;]]
)

minimum_values = (
    df.groupby([&quot;region_code&quot;, &quot;Subcategory&quot;])
    .mean(numeric_only=True)
    .reset_index()
    .groupby([&quot;Subcategory&quot;])
    .apply(lambda x: x.sort_values(ascending=True, by=&quot;hoursPerDayCombined&quot;).head(1))
    .reset_index(drop=True)[[&quot;region_code&quot;, &quot;Subcategory&quot;, &quot;hoursPerDayCombined&quot;]]
)

# Concatenate the maximum and minimum records
concatenated_records = pd.concat([maximum_values, minimum_values], axis=0).reset_index(
    drop=True
)

# Assign flags, country names, and format subcategory names
(
    concatenated_records.assign(
        Subcategory=lambda df: df.Subcategory.str.replace(&quot;_&quot;, &quot; &quot;),
        group=lambda df: df.region_code.mask(
            df.region_code.str.contains(&quot;EU&quot;), True
        ).mask(~df.region_code.str.contains(&quot;EU&quot;), False),
    ).pipe(
        lambda df: ggplot(
            df,
            aes(
                &quot;hoursPerDayCombined&quot;,
                as_discrete(&quot;Subcategory&quot;, order=1, order_by=&quot;hoursPerDayCombined&quot;),
            ),
        )
        + geom_point(aes(color=&quot;group&quot;), size=5, alpha=0.5, show_legend=False)
        + geom_text(
            aes(label=&quot;region_code&quot;),
            position=position_nudge(y=0.25, x=-0),
            size=5,
            na_text=&quot;&quot;,
            angle=15,
            color=&quot;#2b8cbe&quot;,
            tooltips=layer_tooltips(
                [&quot;Subcategory&quot;, &quot;region_code&quot;, &quot;hoursPerDayCombined&quot;]
            ),
        )
        + labs(
            title=&quot;Regions with the Most and Least Time Spent on Each Subcategory&quot;,  # Title
            subtitle=&quot;&quot;&quot;Abbreviations: EU = Europe, AF = Africa, AS = Asia, AM = Americas, ANZ = Australia and New Zealand
            E = East, W = West, N = North, S = South, C = Central, M = Middle
            Values from Europe are highlighted in green.
            &quot;&quot;&quot;,  # Subtitle
            x=&quot;Mean Hours per day combined per Region&quot;,
            y=&quot;&quot;,  # Y-axis label
            caption=&quot;doi.org/10.1073/pnas.2219564120&quot;,  # Caption with the source DOI
        )
        + theme(
            plot_subtitle=element_text(
                size=12, face=&quot;italic&quot;
            ),  # Customize subtitle appearance
            plot_title=element_text(size=15, face=&quot;bold&quot;),  # Customize title appearance
        )
        + scale_x_continuous(
            trans=&quot;log10&quot;, limits=[0.001, 24]
        )  # Set limits for the x-axis
        + ggsize(800, 600)  # Set the plot size
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Which countries allocate the most and least time to each subcategory?&lt;/h1&gt;
&lt;p&gt;As depicted in @fig-fig2 below, on average, the most significant time allocation is dedicated to Sleep and Bedrest, followed by Passive, Interactive, and Social Activities, as well as Activities Centered on Eating and Drinking, including Associated Socializing. While drawing definitive conclusions from this figure may be challenging, Japan stands out as a unique case. They allocate the least amount of time to Sleep and Bedrest while devoting the most time to Passive Activities and Hygiene and Grooming—an interesting observation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
# Calculate the maximum values for each subcategory
maximum_values = (
    df.groupby([&quot;Subcategory&quot;])
    .apply(lambda x: x.sort_values(ascending=False, by=&quot;hoursPerDayCombined&quot;).head(1))[
        [&quot;country_iso3&quot;, &quot;Subcategory&quot;, &quot;hoursPerDayCombined&quot;]
    ]
    .reset_index(drop=True)
    .assign(
        iso2=lambda df: df.country_iso3.map(
            country_regions.set_index(&quot;country_iso3&quot;).country_iso2.squeeze().to_dict()
        )
    )
)

# Calculate the minimum values for each subcategory
minimum_values = (
    df.groupby([&quot;Subcategory&quot;])
    .apply(lambda x: x.sort_values(ascending=True, by=&quot;hoursPerDayCombined&quot;).head(1))[
        [&quot;country_iso3&quot;, &quot;Subcategory&quot;, &quot;hoursPerDayCombined&quot;]
    ]
    .reset_index(drop=True)
    .assign(
        iso2=lambda df: df.country_iso3.map(
            country_regions.set_index(&quot;country_iso3&quot;).country_iso2.squeeze().to_dict()
        )
    )
)

# Concatenate the maximum and minimum records
concatenated_records = pd.concat([maximum_values, minimum_values], axis=0).reset_index(
    drop=True
)

# Assign flags, country names, and format subcategory names
(
    concatenated_records.assign(
        flag=lambda df: df.iso2.apply(flag.flag),
        name=lambda df: df.country_iso3.map(
            country_regions.set_index(&quot;country_iso3&quot;).country_name.squeeze().to_dict()
        ),
        Subcategory=lambda df: df.Subcategory.str.replace(&quot;_&quot;, &quot; &quot;),
    ).pipe(
        lambda df: ggplot(
            df,
            aes(
                &quot;hoursPerDayCombined&quot;,
                as_discrete(&quot;Subcategory&quot;, order=1, order_by=&quot;hoursPerDayCombined&quot;),
            ),
        )
        + geom_point(size=5, alpha=0.25)
        + geom_text(
            aes(label=&quot;flag&quot;),
            position=position_nudge(y=0.5, x=-0),
            size=8,
            na_text=&quot;&quot;,
            angle=15,
            color=&quot;#2b8cbe&quot;,
            tooltips=layer_tooltips([&quot;Subcategory&quot;, &quot;name&quot;, &quot;hoursPerDayCombined&quot;]),
        )
        + labs(
            title=&quot;Countries with the Most and Least Time Spent on Each Subcategory&quot;,  # Title
            subtitle=&quot;&quot;&quot;On average, the highest time allocation is for Sleep and Bedrest, followed by Passive, Interactive, and 
            Social Activities, and Activities Centered on Eating and Drinking, including Associated Socializing&quot;&quot;&quot;,  # Subtitle
            x=&quot;Hours per day combined for the country&quot;,
            y=&quot;&quot;,  # Y-axis label
            caption=&quot;doi.org/10.1073/pnas.2219564120&quot;,  # Caption with the source DOI
        )
        + theme(
            plot_subtitle=element_text(
                size=12, face=&quot;italic&quot;
            ),  # Customize subtitle appearance
            plot_title=element_text(size=15, face=&quot;bold&quot;),  # Customize title appearance
        )
        + scale_x_continuous(limits=[0, 11])  # Set limits for the x-axis
        + ggsize(800, 600)  # Set the plot size
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Is there a discernible relationship between time spent on each category and GDP per capita?&lt;/h1&gt;
&lt;p&gt;Now, let&apos;s make the most of our &apos;gdp_per_population_log&apos; column. Please note that I&apos;m not an economics expert, and our goal here is to identify intriguing trends without delving too deeply into interpretation. For a comprehensive understanding of the dataset, I recommend referring to the original publication.&lt;/p&gt;
&lt;p&gt;Let&apos;s begin by examining the broader trends. We aim to identify activities that appear to exhibit a noteworthy correlation with GDP per population, bearing in mind that GDP per population is log-transformed for our analysis. As evident from @fig-fig3, we&apos;ve come across some intriguing, and perhaps not entirely surprising, observations. For instance, the data hints at a negative relationship between the hours per day spent on schooling and research and GDP per capita. This finding might raise eyebrows, as conventional wisdom often suggests that higher education leads to increased economic output. However, it&apos;s worth considering that spending too much time in the educational system could potentially limit one&apos;s participation in economic production compared to individuals with lower levels of education. It&apos;s a thought-provoking discovery, and while it may challenge some assumptions, it certainly adds depth to our understanding of the data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
(
    df.assign(Subcategory=lambda df: df.Subcategory.str.replace(&quot;_&quot;, &quot; &quot;)).pipe(
        lambda df: ggplot(df, aes(&quot;hoursPerDayCombined&quot;, &quot;gdp_per_population_log&quot;))
        + geom_point(alpha=0.5)
        + geom_smooth()
        + facet_wrap(&quot;Subcategory&quot;, scales=&quot;free&quot;)
        + labs(
            title=&quot;Relationship between Hours per Day Combined and Log(GDP per Population)&quot;,  # Title
            subtitle=&quot;&quot;&quot;Activities vs. GDP per Capita at the country scale. Each circle on the chart 
            represents the population-average time spent per day for an individual country.&quot;&quot;&quot;,
            x=&quot;Hours per day combined&quot;,
            y=&quot;log(GDP per population)&quot;,  # Y-axis label
            caption=&quot;doi.org/10.1073/pnas.2219564120&quot;,  # Caption with the source DOI
        )
        + theme(
            plot_subtitle=element_text(
                size=12, face=&quot;italic&quot;
            ),  # Customize subtitle appearance
            plot_title=element_text(size=15, face=&quot;bold&quot;),  # Customize title appearance
        )
        + ggsize(800, 800)  # Set the plot size
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To further explore the connection between &apos;gdp_per_population_log&apos; and &apos;hoursPerDayCombined,&apos; we can utilize the statsmodels library to execute an Ordinary Least Squares (OLS) regression analysis. This method enables us to compute the β coefficients, in addition to establishing upper and lower confidence intervals, as well as deriving the associated p-values. To streamline this process, we&apos;ll simplify our workflow by creating a function called &lt;code&gt;get_linear_regression_table&lt;/code&gt;. This function will initially filter the data based on the unique subcategory of interest and subsequently apply the formula: {outcome_variable} ~ {independent_variable}.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def get_linear_regression_table(
    data, outcome_variable, independent_variable, category, covariate
):
    &quot;&quot;&quot;
    Calculate linear regression coefficients, confidence intervals, and p-values for each unique category or subcategory
    within the specified category column.

    Parameters:
    - data (pandas.DataFrame): The input DataFrame containing the data.
    - outcome_variable (str): The name of the dependent variable.
    - independent_variable (str): The name of the independent variable.
    - category (str): The column name specifying the category or subcategory for grouping.
                      Should be either &apos;Category&apos; or &apos;Subcategory&apos;.
    - covariate (str or None): The name of the covariate variable to include in the regression model.
                               Set to None if no covariate is needed.

    Returns:
    - results (pandas.DataFrame): A DataFrame containing the results of linear regression analysis for each unique
                                  category or subcategory. Columns include:
      - &apos;unique_subcategory&apos;: The unique category or subcategory.
      - &apos;β&apos;: The coefficient for the independent_variable in the regression model.
      - &apos;Lower CI&apos;: The lower confidence interval for the coefficient.
      - &apos;Upper CI&apos;: The upper confidence interval for the coefficient.
      - &apos;p_value&apos;: The p-value for the coefficient.

    Example usage:
    &amp;gt;&amp;gt;&amp;gt; results = get_linear_regression_table(data, &apos;OutcomeVar&apos;, &apos;IndependentVar&apos;, &apos;Category&apos;, &apos;CovariateVar&apos;)
    &quot;&quot;&quot;
    results_list = []

    if category == &quot;Subcategory&quot;:
        cat = &quot;Subcategory&quot;
        unique_categories = data[cat].unique().tolist()
    else:
        cat = &quot;Category&quot;
        unique_categories = data[cat].unique().tolist()

    for unique_category in unique_categories:
        temp_df = data.loc[lambda df: df[cat] == unique_category, :]

        if covariate is not None:
            # Fit the linear regression model with covariate
            reg = smf.ols(
                f&quot;{outcome_variable} ~ {covariate} + {independent_variable}&quot;,
                data=temp_df,
            ).fit(disp=0)
        else:
            # Fit the linear regression model without covariate
            reg = smf.ols(
                f&quot;{outcome_variable} ~ {independent_variable}&quot;, data=temp_df
            ).fit(disp=0)

        # Get the regression coefficients, confidence intervals, and p-values
        beta = reg.params[-1]
        lower_CI = reg.conf_int()[0][-1]
        upper_CI = reg.conf_int()[1][-1]
        pvalue = reg.pvalues[-1]

        # Append the results to the list as a dictionary
        results_list.append(
            {
                &quot;unique_subcategory&quot;: unique_category,
                &quot;β&quot;: beta,
                &quot;Lower CI&quot;: lower_CI,
                &quot;Upper CI&quot;: upper_CI,
                &quot;p_value&quot;: pvalue,
            }
        )

    # Convert the list of dictionaries into a DataFrame
    results = pd.DataFrame(results_list)
    return results
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;According to our linear regression analysis, it appears that activities like Materials (involving the extraction of substances for various purposes), Food Processing (which includes food transformation and preservation), and Food Growth &amp;amp; Collection (encompassing activities related to edible organic matter) exhibit the most pronounced negative relationships with GDP per capita. Specifically, the β coefficients for these activities are -6.21, -2.86, and -1.03, respectively, with all associated p-values falling below the 0.05 threshold.&lt;br /&gt;
Conversely, we observe the most robust positive relationships with GDP per capita in the case of Active Recreation (involving activities with elevated metabolic activity), Infrastructure (associated with the construction and maintenance of transportation and communication structures), and Waste Management (pertaining to the handling of waste and by-products outside of inhabited areas). These activities exhibit β coefficients of 2.1, 12.17, and 25.17, respectively, with all corresponding p-values falling below the 0.05 significance level.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(
    get_linear_regression_table(
        df,
        outcome_variable=&quot;gdp_per_population_log&quot;,
        independent_variable=&quot;hoursPerDayCombined&quot;,
        category=&quot;Subcategory&quot;,
        covariate=None,
    )
    .assign(unique_subcategory=lambda df: df.unique_subcategory.str.replace(&quot;_&quot;, &quot; &quot;))
    .sort_values([&quot;β&quot;, &quot;p_value&quot;])
    .style.format(precision=3, thousands=&quot;,&quot;, decimal=&quot;.&quot;)
    .applymap(
        lambda value: &quot;color: red; font-weight:bold&quot; if (value &amp;lt;= 0.05) else None, subset=[&quot;p_value&quot;]
    )
    .background_gradient(cmap=&quot;viridis&quot;, axis=0, subset=&quot;β&quot;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Although the existing analysis provides valuable insights, we can enhance our understanding further by building a model that considers both regions and different subcategories simultaneously. To achieve this, we&apos;ll re-run our regression model with &apos;gdp_per_population_log&apos; as the outcome variable and &apos;hoursPerDayCombined&apos; as the independent variable. The &apos;region_code&apos; will serve as a categorical variable, with Western Europe set as the reference level. Additionally, &apos;Subcategory&apos; will also be treated as a categorical variable, with &apos;Passive Activities&apos; chosen as the reference level. This approach will help us determine whether the hours spent on various activities exhibit any discernible relationship with GDP per capita.&lt;/p&gt;
&lt;p&gt;As evident from the table below, none of the coefficients associated with the various subcategories showed significant differences compared to zero. This suggests that these subcategories do not exert a substantial effect on &apos;gdp_per_population_log.&apos; Additionally, the β coefficient for &apos;hoursPerDayCombined&apos; was also not statistically distinct from zero.&lt;/p&gt;
&lt;p&gt;Conversely, when considering the impact of regions, we observe a substantial influence on the correlation. Most regions exhibit negative coefficients in comparison to Western Europe. The exceptions to this trend are North America, New Zealand, and Australia, where the corresponding p-values exceeded 0.05. These findings suggest that the region indeed plays a significant role in the correlation with &apos;gdp_per_population_log.&apos;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;res = smf.ols(
    &quot;&quot;&quot;gdp_per_population_log ~  hoursPerDayCombined
+ C(region_code, Treatment(reference=&apos;EU_W&apos;)) 
+ C(Subcategory, Treatment(reference=&apos;Passive&apos;)) 
&quot;&quot;&quot;,
    data=df,
).fit(disp=0)

res.summary2().tables[1]
(
    res.summary2()
    .tables[1]
    .style.format(precision=3, thousands=&quot;,&quot;, decimal=&quot;.&quot;)
    .applymap(
        lambda value: &quot;color: red; font-weight:bold&quot; if (value &amp;lt;= 0.05) else None, subset=[&quot;P&amp;gt;|t|&quot;]
    )
    .background_gradient(cmap=&quot;viridis&quot;, axis=0, subset=&quot;Coef.&quot;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Regression diagnostic&lt;/h2&gt;
&lt;p&gt;Next, we&apos;ll perform some diagnostic procedures for our regression model. These diagnostics will serve the purpose of evaluating how well our model aligns with its underlying assumptions. This step is crucial for ensuring the reliability and validity of our regression analysis.&lt;br /&gt;
As depicted in @fig-fig4, our residuals exhibit a relatively normal distribution. While there are a few data points that may have contributed to some erroneous predictions, we have opted to maintain the current state of the analysis as the results appear reasonable.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
# Create a histogram plot of residuals
hist_plot = res.resid.to_frame(name=&quot;residuals&quot;).pipe(
    lambda df: ggplot(df, aes(&quot;residuals&quot;))
    + geom_histogram(bins=15)
    + labs(
        title=&quot;Histogram of Residuals&quot;,
    )
    + theme(plot_title=element_text(size=15, face=&quot;bold&quot;))
)

# Create a residuals plot
residuals_plot = (
    res.resid.to_frame(name=&quot;residuals&quot;)
    .reset_index()  # Reset the index for plotting
    .pipe(
        lambda df: ggplot(df, aes(&quot;index&quot;, &quot;residuals&quot;))
        + geom_point(size=5, alpha=0.05)  # Plot residuals as points
        + labs(
            title=&quot;Residuals&quot;,  # Title for the residuals plot
        )
        + theme(
            plot_title=element_text(size=15, face=&quot;bold&quot;)  # Customize title appearance
        )
    )
)


# Create a Q-Q plot of residuals
qqplot = res.resid.to_frame(name=&quot;residuals&quot;).pipe(
    lambda df: qq_plot(data=df, sample=&quot;residuals&quot;, size=5, alpha=0.05)
    + labs(
        title=&quot;Q-Q Plot&quot;,
    )
    + theme(plot_title=element_text(size=15, face=&quot;bold&quot;))
)

# Create a fit plot comparing predicted values and log(GDP per population)
fit_plot = (
    pd.concat([res.fittedvalues, df.gdp_per_population_log], axis=1)
    .rename(
        columns={
            0: &quot;Predicted value&quot;,
            &quot;gdp_per_population_log&quot;: &quot;log(GDP per population)&quot;,
        }
    )
    .pipe(
        lambda df: ggplot(df, aes(&quot;Predicted value&quot;, &quot;log(GDP per population)&quot;))
        + geom_point(size=5, alpha=0.05)
        + labs(
            title=&quot;Fit Plot&quot;,
        )
        + theme(plot_title=element_text(size=15, face=&quot;bold&quot;))
        + geom_abline(slope=1, size=1, linetype=&quot;dashed&quot;, color=&quot;red&quot;)
    )
)

# Combine the three plots into a grid with two columns
(gggrid([hist_plot, residuals_plot, qqplot, fit_plot], ncol=2) + ggsize(800, 600))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And there you have it, folks! We hope you&apos;ve found this week&apos;s exploration insightful. Happy coding to all, and I look forward to seeing you next week for more exciting discoveries and learning opportunities. Until then, stay curious and keep exploring! 👩‍💻👨‍💻&lt;/p&gt;
</content:encoded><author>Adam Cseresznye</author></item><item><title>Speed up your Python code 60x</title><link>https://adamcseresznye.github.io/posts/speed-up-py</link><guid isPermaLink="true">https://adamcseresznye.github.io/posts/speed-up-py</guid><pubDate>Fri, 07 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1614729375290-b2a429db839b?q=80&amp;amp;w=1963&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;ixlib=rb-4.0.3&amp;amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&quot; alt=&quot;Photo by NASA on UnSplash&quot; title=&quot;Space Shuttle Columbia&apos;s STS-4 mission launched from Kennedy Space Center on June 27, 1982&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Ever felt the need to turbo boost your Python code? Tired of the seemingly endless wait for your iterations or simulations to complete? Allow me to present the &lt;a href=&quot;https://github.com/mityax/rustimport&quot;&gt;&lt;code&gt;rustimport&lt;/code&gt;&lt;/a&gt; library. In its most basic form, &lt;code&gt;rustimport&lt;/code&gt; empowers you to call highly optimized Rust functions from within your code. This way, you can delegate the heavy-duty tasks to Rust, instead of relying on Python, a language known for its versatility but also for its slower speed. With &lt;code&gt;rustimport&lt;/code&gt;, you can enjoy the best of both worlds - the simplicity of Python and the performance of Rust.&lt;/p&gt;
&lt;p&gt;Let me give you an example: imagine you’re tackling one of the problems from &lt;a href=&quot;https://projecteuler.net/problem=50&quot;&gt;Project Euler&lt;/a&gt;, specifically problem #50. Here’s what the problem statement looks like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The prime $41$, can be written as the sum of six consecutive primes:
$$41 = 2 + 3 + 5 + 7 + 11 + 13.$$
This is the longest sum of consecutive primes that adds to a prime below one-hundred.
The longest sum of consecutive primes below one-thousand that adds to a prime, contains $21$ terms, and is equal to $953$.
Which prime, below one-million, can be written as the sum of the most consecutive primes?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;While there are many ways to solve this problem, one approach (though not the fastest) is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import cProfile
import pstats
import io


def is_prime(n):
    if n &amp;lt;= 1:
        return False
    i = 2
    while i * i &amp;lt;= n:
        if n % i == 0:
            return False
        i += 1
    return True


def generate_primes(target):
    primes = [n for n in range(2, target + 1) if is_prime(n)]
    return primes


def longest_sum_of_consecutive_primes(primes, target):
    max_length = 0
    max_prime = 0

    for i in range(len(primes)):
        for j in range(i + max_length, len(primes)):
            sum_of_primes = sum(primes[i:j])
            if sum_of_primes &amp;gt; target:
                break
            if is_prime(sum_of_primes) and (j - i) &amp;gt; max_length:
                max_length = j - i
                max_prime = sum_of_primes

    return max_prime, max_length


def main():
    TARGET = 1_000_000
    primes = generate_primes(TARGET)
    max_prime, max_length = longest_sum_of_consecutive_primes(primes, TARGET)
    print(f&quot;Prime: {max_prime}, Length: {max_length}&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s dive in the code quickly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;is_prime(n)&lt;/code&gt; function checks if a number n is prime. It starts by checking if n is less than or equal to 1, in which case it returns False as these are not prime numbers. It then checks divisibility from 2 up to the square root of n. If n is divisible by any of these numbers, it returns False; otherwise, it returns True indicating that n is a prime number.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;generate_primes(target)&lt;/code&gt; function generates a list of prime numbers up to a given target number, which is in our case 1000. It uses a list comprehension to iterate over the range from 2 to target (inclusive), calling the &lt;code&gt;is_prime(n)&lt;/code&gt; function for each number n in the range. The result is a list of prime numbers.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;longest_sum_of_consecutive_primes(primes, target)&lt;/code&gt; function finds the prime number below a given target that can be written as the sum of the most consecutive primes. It starts with two variables, &lt;code&gt;max_length&lt;/code&gt; and &lt;code&gt;max_prime&lt;/code&gt;, initialized to 0. It then iterates over the primes list, for each prime calculating the sum of primes from the current prime to the end of the list. If the sum exceeds the target, it breaks the inner loop. If the sum is prime and the length of the sequence is greater than &lt;code&gt;max_length&lt;/code&gt;, it updates &lt;code&gt;max_length&lt;/code&gt; and &lt;code&gt;max_prime&lt;/code&gt;. The function finally returns &lt;code&gt;max_prime&lt;/code&gt; and &lt;code&gt;max_length&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt; To evaluate the execution time and identify which functions take longer to run, we use &lt;code&gt;cProfile&lt;/code&gt;. The statistics generated are then formatted using the &lt;code&gt;pstats&lt;/code&gt; module. The complete code for all the examples can be found &lt;a href=&quot;https://github.com/adamcseresznye/speed_up_py&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    1157601 function calls in 5.517 seconds

       Ordered by: cumulative time

       ncalls  tottime  percall  cumtime  percall filename:lineno(function)
            1    0.000    0.000    5.517    5.517 PE50_v1.py:36(main)
            1    0.000    0.000    5.025    5.025 PE50_v1.py:17(generate_primes)
            1    0.134    0.134    5.025    5.025 PE50_v1.py:18(&amp;lt;listcomp&amp;gt;)
      1000570    4.894    0.000    4.894    0.000 PE50_v1.py:7(is_prime)
            1    0.288    0.288    0.491    0.491 PE50_v1.py:21(longest_sum_of_consecutive_primes)
        78526    0.197    0.000    0.197    0.000 {built-in method builtins.sum}
        78499    0.004    0.000    0.004    0.000 {built-in method builtins.len}
            1    0.000    0.000    0.000    0.000 {built-in method builtins.print}
            1    0.000    0.000    0.000    0.000 {method &apos;disable&apos; of &apos;_lsprof.Profiler&apos; objects}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, this straightforward task required over 5.5 seconds to execute. The primary function, &lt;code&gt;PE50_v1.py:36(main)&lt;/code&gt;, was invoked once and took a total of 5.517 seconds. The majority of this time was spent calling the function &lt;code&gt;PE50_v1.py:17(generate_primes)&lt;/code&gt;, which itself spent most of its time calling &lt;code&gt;PE50_v1.py:7(is_prime)&lt;/code&gt; over a million times. The function &lt;code&gt;PE50_v1.py:21(longest_sum_of_consecutive_primes)&lt;/code&gt; also consumed a significant amount of time. Additionally, the built-in functions &lt;code&gt;sum&lt;/code&gt; and &lt;code&gt;len&lt;/code&gt; were called multiple times.&lt;/p&gt;
&lt;h2&gt;Replacing the &lt;code&gt;is_prime&lt;/code&gt; function&lt;/h2&gt;
&lt;p&gt;From the profiling results, it&apos;s clear that &lt;code&gt;is_prime&lt;/code&gt; is a prime candidate (pun intended) for replacement with a Rust equivalent. Let&apos;s explore how we can achieve this.&lt;/p&gt;
&lt;p&gt;Firstly, we need to install the &lt;code&gt;rustimport&lt;/code&gt; library. This can be done using pip: &lt;code&gt;pip install rustimport&lt;/code&gt;. Following this, we can create a Rust file, which we&apos;ll name &lt;code&gt;rs_extension.rs&lt;/code&gt; for this example. Now, let&apos;s write some Rust code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// rustimport:pyo3

use pyo3::prelude::*;

#[pyfunction]
fn is_prime(n: u32) -&amp;gt; bool {
    if n &amp;lt;= 1 {
        return false;
    }
    let mut i = 2;
    while i * i &amp;lt;= n {
        if n % i == 0 {
            return false;
        }
        i += 1;
    }
    true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Upon comparing the Python &lt;code&gt;is_prime&lt;/code&gt; function with its Rust counterpart, you&apos;ll notice some similarities. The key difference comes from Rust&apos;s statically and strongly typed system, which requires us to specify the type of the input variables and return types. Not to worry, it&apos;s simple; we anticipate an unsigned integer (positive integers only) and return a boolean value indicating whether or not the integer is prime. Let&apos;s see what kind of speed up we get.&lt;/p&gt;
&lt;p&gt;:::warning
If your Rust code isn&apos;t performing as expected, it could be because it was compiled in debug mode, which doesn&apos;t enable optimizations. However, by running the &lt;code&gt;python -m rustimport build --release&lt;/code&gt; command, you can generate Rust code that is highly optimized and significantly faster. Give it a try!`.
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;         1157601 function calls in 0.641 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.641    0.641 PE50_v1.py:30(main)
        1    0.197    0.197    0.391    0.391 PE50_v1.py:14(longest_sum_of_consecutive_primes)
        1    0.000    0.000    0.249    0.249 PE50_v1.py:9(generate_primes)
        1    0.089    0.089    0.249    0.249 PE50_v1.py:10(&amp;lt;listcomp&amp;gt;)
    78526    0.191    0.000    0.191    0.000 {built-in method builtins.sum}
  1000570    0.160    0.000    0.160    0.000 {built-in method rs_extension.is_prime}
    78499    0.004    0.000    0.004    0.000 {built-in method builtins.len}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.print}
        1    0.000    0.000    0.000    0.000 {method &apos;disable&apos; of &apos;_lsprof.Profiler&apos; objects}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🤯🤯🤯🤯 Our execution time went from 5.517 seconds to 0.641 seconds 🚀. That&apos;s a whopping 8.6 times speed up, achieved by replacing just one function. The &lt;code&gt;is_prime&lt;/code&gt; function now takes a total of 0.16 seconds to execute. But why stop here? We can push the boundaries even further.&lt;/p&gt;
&lt;p&gt;Consider the &lt;code&gt;longest_sum_of_consecutive_primes&lt;/code&gt; function. It performs a substantial amount of work, and if we could harness the power of Rust for this function, we could potentially slash the execution time even more. Let´s give it a try.&lt;/p&gt;
&lt;h2&gt;Replacing the &lt;code&gt;longest_sum_of_consecutive_primes&lt;/code&gt; function&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#[pyfunction]
fn longest_sum_of_consecutive_primes(primes: Vec&amp;lt;u32&amp;gt;, target: u32) -&amp;gt; (u32, usize) {
    let mut max_length: usize = 0;
    let mut max_prime: u32 = 0;

    for i in 0..primes.len() {
        for j in i + max_length..primes.len() {
            let sum_of_primes: u32 = primes[i..j].iter().sum();
            if sum_of_primes &amp;gt; target {
                break;
            }
            if is_prime(sum_of_primes) &amp;amp;&amp;amp; (j - i) &amp;gt; max_length {
                max_length = j - i;
                max_prime = sum_of_primes;
            }
        }
    }
    (max_prime, max_length)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now this Rust function looks quite a bit different from our original Python function, but it&apos;s still possible to understand what&apos;s going on even if you haven&apos;t seen Rust code before. In this case, we&apos;re supplying the function with a vector of prime numbers, each represented as &lt;code&gt;u32&lt;/code&gt; values, and a target number, which is 1000 in our case. The function then returns the solution to our problem: the number (&lt;code&gt;max_prime&lt;/code&gt;) that can be expressed as the sum of the greatest number of consecutive primes, and the actual count of these consecutive primes (&lt;code&gt;max-length&lt;/code&gt;). This is the result from our second optimization:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;         1000005 function calls in 0.252 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.252    0.252 PE50_v2.py:14(main)
        1    0.000    0.000    0.248    0.248 PE50_v2.py:9(generate_primes)
        1    0.088    0.088    0.248    0.248 PE50_v2.py:10(&amp;lt;listcomp&amp;gt;)
   999999    0.160    0.000    0.160    0.000 {built-in method rs_extension.is_prime}
        1    0.004    0.004    0.004    0.004 {built-in method rs_extension.longest_sum_of_consecutive_primes}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.print}
        1    0.000    0.000    0.000    0.000 {method &apos;disable&apos; of &apos;_lsprof.Profiler&apos; objects}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🤯🤯🤯🤯 Our execution time has now been reduced to just 0.252 seconds, a further improvement of 2.5 times. In total, we&apos;ve achieved a staggering 21.5 times speedup compared to our original Python implementation. The &lt;code&gt;longest_sum_of_consecutive_primes&lt;/code&gt; function, which previously took 0.391 seconds to execute, now completes in a mere 0.004 seconds. This is a significant achievement!&lt;/p&gt;
&lt;p&gt;As you might have guessed, we&apos;re not stopping here 😈. We still have one more Python function left to convert to Rust 🦀. Let&apos;s do that and see how much more we can optimize our code!&lt;/p&gt;
&lt;h2&gt;Replacing the &lt;code&gt;generate_primes&lt;/code&gt; function&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#[pyfunction]
fn generate_primes(target: u32) -&amp;gt; Vec&amp;lt;u32&amp;gt; {
    (2..=target).filter(|&amp;amp;n| is_prime(n)).collect::&amp;lt;Vec&amp;lt;u32&amp;gt;&amp;gt;()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This Rust code, while seemingly simple, is quite powerful. It generates, or more accurately, filters out prime numbers up to a specified range. Given that Rust is heavily inspired by OCaml and Haskell, you may notice the influence of functional programming paradigms throughout the language (and in this example).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;         5 function calls in 0.092 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.092    0.092 PE50_v3.py:9(main)
        1    0.088    0.088    0.088    0.088 {built-in method rs_extension.generate_primes}
        1    0.004    0.004    0.004    0.004 {built-in method rs_extension.longest_sum_of_consecutive_primes}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.print}
        1    0.000    0.000    0.000    0.000 {method &apos;disable&apos; of &apos;_lsprof.Profiler&apos; objects}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;😲😵‍💫🤯💥💥💥 And there you have it! 🎉 Rust did it again. The execution time has been slashed from 0.252 seconds to a mere 0.092 seconds, making it 2.7 times faster. The &lt;code&gt;generate_primes&lt;/code&gt; function, which previously took 0.248 seconds, now completes in just 0.088 seconds. And thus our code went from taking 5.517 sec to just 0.092 sec...&lt;/p&gt;
&lt;p&gt;Here is our final iteration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import cProfile
import pstats
import io

# import rustimport.import_hook
import rs_extension


def main():
    TARGET = 1_000_000
    primes = rs_extension.generate_primes(TARGET)
    max_prime, max_length = rs_extension.longest_sum_of_consecutive_primes(
        primes, TARGET
    )
    print(f&quot;Prime: {max_prime}, Length: {max_length}&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import pandas as pd
import plotly.express as px

df = pd.DataFrame(
    {
        &quot;step&quot;: [
            &quot;original&quot;,
            &quot;rewrite is_prime&quot;,
            &quot;rewrite longest_sum_of_consecutive_primes&quot;,
            &quot;rewrite generate_primes&quot;,
        ],
        &quot;execution_time&quot;: [5.517, 0.641, 0.252, 0.092],
    }
)
fig = px.bar(
    df,
    x=&quot;execution_time&quot;,
    y=&quot;step&quot;,
    color=&quot;step&quot;,
    title=&quot;Turbocharging Performance: A 60x Speed Boost with Rust!&quot;,
    orientation=&quot;h&quot;,
    template=&quot;plotly_white&quot;,
)
fig.update_yaxes(title=&quot;&quot;, visible=True)
fig.update_xaxes(title=&quot;Execution time (s)&quot;)
fig.update_layout(showlegend=False)


fig.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&apos;s a wrap, everyone! In this post, we&apos;ve showed you how to use &lt;code&gt;cProfile&lt;/code&gt; to profile your Python code, helping you pinpoint potential bottlenecks. We&apos;ve also showcased the power of the &lt;code&gt;rustimport&lt;/code&gt; library, which enables you to effortlessly invoke highly optimized Rust code straight from your Python script.&lt;/p&gt;
&lt;p&gt;Have you had the chance to use these tools in your projects? Do you believe your work could benefit from Rust? I&apos;d love to hear your thoughts. Until next time, happy coding!&lt;/p&gt;
</content:encoded><author>Adam Cseresznye</author></item></channel></rss>