<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: kitchen_code</title>
    <description>The latest articles on DEV Community by kitchen_code (@kitchen_code).</description>
    <link>https://dev.to/kitchen_code</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4036824%2F0ef35a91-8d0b-4805-b904-c03de4bdd7d6.png</url>
      <title>DEV Community: kitchen_code</title>
      <link>https://dev.to/kitchen_code</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kitchen_code"/>
    <language>en</language>
    <item>
      <title>Building an Online Retail Sales &amp; Cancellation Analysis Report With Power BI</title>
      <dc:creator>kitchen_code</dc:creator>
      <pubDate>Thu, 27 Aug 2026 08:34:37 +0000</pubDate>
      <link>https://dev.to/kitchen_code/power-bi-online-retail-sales-cancellation-analysis-53b9</link>
      <guid>https://dev.to/kitchen_code/power-bi-online-retail-sales-cancellation-analysis-53b9</guid>
      <description>&lt;p&gt;This article targets junior data engineers and data analysts who are familiar with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ELT pipelines&lt;/li&gt;
&lt;li&gt;Data warehouses&lt;/li&gt;
&lt;li&gt;Star schemas&lt;/li&gt;
&lt;li&gt;Snowflake&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article demonstrates basic data analysis and visualization in Power BI using the &lt;a href="https://dev.to/kitchen_code/desining-and-building-an-online-retail-data-warehouse-with-dbt-and-snowflake-5de7"&gt;Online Retail data warehouse&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: This analysis uses the &lt;a href="https://archive.ics.uci.edu/dataset/352/online+retail" rel="noopener noreferrer"&gt;Online Retail dataset&lt;/a&gt; by Daqing Chen, made available through the &lt;a href="https://archive.ics.uci.edu/" rel="noopener noreferrer"&gt;UCI Machine Learning Repository&lt;/a&gt; under the &lt;a href="https://creativecommons.org/licenses/by/4.0/legalcode" rel="noopener noreferrer"&gt;CC BY 4.0&lt;/a&gt; license.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Power BI: Online Retail Sales &amp;amp; Cancellation Analysis
&lt;/h1&gt;

&lt;h2&gt;
  
  
  1. Connect Power BI to Snowflake
&lt;/h2&gt;

&lt;p&gt;I start by creating a Power BI role in Snowflake using the following statements.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;POWERBI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="n"&gt;ONLINE_RETAIL_WAREHOUSE&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;POWERBI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;ONLINE_RETAIL_DB&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;POWERBI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;ONLINE_RETAIL_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RETAIL&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;POWERBI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;ONLINE_RETAIL_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RETAIL&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;POWERBI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These queries create &lt;code&gt;POWERBI&lt;/code&gt;, which is a new Power BI role. Then I grant the role privileges on the &lt;code&gt;online_retail_warehouse&lt;/code&gt; and &lt;code&gt;online_retail_db&lt;/code&gt; objects.&lt;/p&gt;

&lt;p&gt;In Power BI, I select Snowflake as the data source option, as shown in Figure 1.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fruir4k2mr6nlwpvmt40k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fruir4k2mr6nlwpvmt40k.png" alt="Figure 1 shows the Snowflake selection as the data source in Power BI." width="800" height="563"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1. Select Snowflake as the data source in Power BI.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next, I fill the required inputs to establish the connection with Snowflake. Figure 2 is an example of this step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjlrxrur2e3rabxmfaj1w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjlrxrur2e3rabxmfaj1w.png" alt="Figure 2 shows Snowflake credentials submission to establish connection." width="800" height="562"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2. Submit Snowflake credentials to establish connection.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The required inputs are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A server value, which is found in the account information section in Snowsight.&lt;/li&gt;
&lt;li&gt;A warehouse identifier, which in this case is &lt;code&gt;online_retail_warehouse&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An optional role, which I created in the previous SQL statements. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the connection is established, I select the fact table and dimension tables, and choose the load option. Figure 3 is a representation of this step. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frdwasnz6honbvuv0uf9z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frdwasnz6honbvuv0uf9z.png" alt=" " width="800" height="563"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3. Select the Online Retail fact table and dimension tables.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next I choose the load method as shown in Figure 4.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F805mqew7pvm2yqbwm0nm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F805mqew7pvm2yqbwm0nm.png" alt="Figure 4 shows the load method selection step." width="799" height="562"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 4. Select Import method.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I choose Import because the data is relatively small and static. The Power BI Import option creates a copy of the data in its in-memory VertiPaq storage engine to ensure faster performance. &lt;/p&gt;

&lt;p&gt;Figure 5 shows the imported tables in the Power BI data pane, confirming that the data has successfully been loaded. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuv63psr1k76y18orbtiv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuv63psr1k76y18orbtiv.png" alt="Figure 5 shows imported Online Retail tables in Power BI." width="800" height="562"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 5. Imported Online Retail tables in Power BI.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Verify the Star Schema
&lt;/h2&gt;

&lt;p&gt;Figure 6 shows the star schema inside the Power BI model section.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fytgl84za8wwzr9hc4db0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fytgl84za8wwzr9hc4db0.png" alt="Figure 6 shows Online Retail star schema on Power BI." width="799" height="421"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 6. Online Retail star schema on Power BI.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I verify that &lt;code&gt;fact_transaction&lt;/code&gt; and its relationships with the dimension tables are correctly defined. &lt;/p&gt;
&lt;h2&gt;
  
  
  3. Define the business questions
&lt;/h2&gt;

&lt;p&gt;To guide the analysis, I organize the business questions in four categories: customer analysis, time-based cancellation analysis, product cancellation analysis, geographic cancellation analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customer Analysis&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which customers generate the most cancelled transactions?&lt;/li&gt;
&lt;li&gt;Which customers generate the most transactions while having the lowest cancellation rate?&lt;/li&gt;
&lt;li&gt;Which customers generate the most revenue?&lt;/li&gt;
&lt;li&gt;Which customers purchase the largest quantity of products?&lt;/li&gt;
&lt;li&gt;Which customers purchase products with the highest average price per unit?&lt;/li&gt;
&lt;li&gt;Which customers generate high revenue while maintaining a low cancellation rate?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Time-based Cancellation Analysis&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which periods experience the highest number of cancellations?&lt;/li&gt;
&lt;li&gt;How does the cancellation rate vary throughout the year?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Product Cancellation Analysis&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which products are most frequently associated with cancellations?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Geographic Cancellation Analysis&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which countries have the highest cancellation rate, and which have the lowest?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  4. Define the analytical measures
&lt;/h2&gt;

&lt;p&gt;I define the following analytical measures required to answer the business questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total transactions&lt;/strong&gt;: total number of transactions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancelled transactions&lt;/strong&gt;: number of cancelled transactions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cancellation rate&lt;/strong&gt;: percentage of transactions that were cancelled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total revenue&lt;/strong&gt;: total revenue generated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total quantity&lt;/strong&gt;: total quantity of products purchased.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average unit price&lt;/strong&gt;: average price per unit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the measures identified, I can now implement them in Power BI using Data Analysis Expressions as follows.&lt;/p&gt;

&lt;p&gt;In order to create a new measure, I right-click the fact table in the model view on Power BI, and select &lt;strong&gt;New measure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Total transactions&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;COUNTROWS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FACT_TRANSACTION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use the &lt;code&gt;COUNTROWS&lt;/code&gt; aggregation function to count all the rows in &lt;code&gt;fact_transaction&lt;/code&gt; and return the total number of transactions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cancelled transactions&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Cancelled&lt;/span&gt; &lt;span class="n"&gt;Transactions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="n"&gt;CALCULATE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;COUNTROWS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FACT_TRANSACTION&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;FACT_TRANSACTION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;is_cancelled&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use the &lt;code&gt;CALCULATE&lt;/code&gt; function to evaluate the &lt;code&gt;COUNTROWS&lt;/code&gt; function under a modified filter context. The filter checks the value of the &lt;code&gt;is_cancelled&lt;/code&gt; column, and only counts rows where its value is &lt;code&gt;True&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cancellation rate&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Cancellation&lt;/span&gt; &lt;span class="n"&gt;Rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DIVIDE&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Cancelled&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To calculate the cancellation rate, I use the &lt;code&gt;DIVIDE&lt;/code&gt; function to divide the &lt;code&gt;Cancelled transactions&lt;/code&gt; measure by the &lt;code&gt;Total transaction&lt;/code&gt; measure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Total revenue&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;Revenue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'FACT_TRANSACTION'&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;total_cost&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use the &lt;code&gt;SUM&lt;/code&gt; aggregation function to calculate the sum of the values in the &lt;code&gt;total_cost&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Total quantity&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'FACT_TRANSACTION'&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;QUANTITY&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use the &lt;code&gt;SUM&lt;/code&gt; aggregation function to calculate the sum of the values in the &lt;code&gt;quantity&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Average unit price&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Average&lt;/span&gt; &lt;span class="n"&gt;Unit&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AVERAGE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'FACT_TRANSACTION'&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UNIT_PRICE&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use the &lt;code&gt;AVERAGE&lt;/code&gt; aggregation function to calculate the arithmetic mean of the &lt;code&gt;unit_price&lt;/code&gt; values.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Build the visualizations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 Customer Analysis
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Which customers generate the most cancelled transactions?
&lt;/h4&gt;

&lt;p&gt;To answer this question, I create a clustered bar chart. I navigate to the Report view in Power BI. Figure 7 shows the resulting visualization.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ddyw7vnb2bbrd75spee.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ddyw7vnb2bbrd75spee.png" alt="Figure 7 represents the top ten customers by cancelled transactions." width="663" height="436"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 7. Clustered bar chart representing the Top ten customers by cancelled transactions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I select the &lt;code&gt;Cancelled transactions&lt;/code&gt; measure for the X-axis. The Y-axis represents &lt;code&gt;customer_id&lt;/code&gt; from &lt;code&gt;dim_customer&lt;/code&gt;. I filter the chart to display the top ten customers with the highest number of cancelled transactions.&lt;/p&gt;

&lt;h4&gt;
  
  
  Which customers generate the most transactions while having the lowest cancellation rate?
&lt;/h4&gt;

&lt;p&gt;I choose a scatter chart to represent the transaction volume vs cancellation rate by customer. Figure 8 shows the resulting visualization. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foo25gcp9zdp1q7me9m85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foo25gcp9zdp1q7me9m85.png" alt="Figure 8 represents the transaction volume vs cancellation rate by customer." width="542" height="391"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 8. Scatter chart representing the transaction volume vs. cancellation rate by customer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I select the &lt;code&gt;Total transaction&lt;/code&gt; measure for the X-axis, the &lt;code&gt;Cancellation Rate&lt;/code&gt; measure for the Y-axis, and &lt;code&gt;customer_id&lt;/code&gt; from &lt;code&gt;dim_customer&lt;/code&gt; for the value.&lt;/p&gt;

&lt;p&gt;Customers with the highest total transaction volumes and the lowest cancellation rates are positioned toward the bottom-right.&lt;/p&gt;

&lt;h4&gt;
  
  
  Which customers generate the most revenue?
&lt;/h4&gt;

&lt;p&gt;I add a clustered bar chart to visualize the customers who generate the most revenue. Figure 9 shows the resulting visualization.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2tag20jima4xuq1blv6a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2tag20jima4xuq1blv6a.png" alt="Figure 9 represents the top ten customers by total revenue." width="498" height="378"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 9. Clustered bar chart representing the top ten customers by total revenue.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I select the &lt;code&gt;Total Revenue&lt;/code&gt; measure for the X-axis, while the Y-axis represents &lt;code&gt;customer_id&lt;/code&gt; from &lt;code&gt;dim_customer&lt;/code&gt;. I filter the chart to display the top ten customers with the highest total revenue.&lt;/p&gt;

&lt;h4&gt;
  
  
  Which customers purchase the largest quantity of products?
&lt;/h4&gt;

&lt;p&gt;I create a clustered bar chart to represent the customers who purchased the largest quantities of products. Figure 10 shows the resulting visualization.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5oz6yfs7zd3xq0ynqvw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5oz6yfs7zd3xq0ynqvw.png" alt="Figure 10 represents top ten customers by total quantity." width="499" height="392"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 10. Clustered bar chart representing the top ten customers by total quantities purchased.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I select the &lt;code&gt;Total Quantity&lt;/code&gt; measure for the X-axis, and &lt;code&gt;customer_id&lt;/code&gt; from &lt;code&gt;dim_customer&lt;/code&gt; for the Y-axis.&lt;/p&gt;

&lt;h4&gt;
  
  
  Which customers purchase products with the highest average price per unit?
&lt;/h4&gt;

&lt;p&gt;I add a clustered bar chart to visualize the customers who purchased the products with the highest average price per unit. Figure 11 shows the resulting visualization.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgzsxr4bgrxa1eoux9hr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgzsxr4bgrxa1eoux9hr.png" alt="Figure 11 represents top ten customers by average price per unit." width="488" height="413"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 11. Clustered bar chart representing the top ten customers by average price per unit.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I select the &lt;code&gt;Average Unit Price&lt;/code&gt; measure for the X-axis, and &lt;code&gt;customer_id&lt;/code&gt; from &lt;code&gt;dim_customer&lt;/code&gt; for the Y-axis.&lt;/p&gt;

&lt;h4&gt;
  
  
  Which customers generate high revenue while maintaining a low cancellation rate?
&lt;/h4&gt;

&lt;p&gt;I select a scatter chart to represent the revenue vs the cancellation rate by customer. Figure 12 shows the resulting visualization.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fygq34lwfb9zexhho2efa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fygq34lwfb9zexhho2efa.png" alt="Figure 12 represents revenue vs cancellation rate by customer." width="554" height="446"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 12. Scatter chart representing revenue vs. cancellation rate by customer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I select the &lt;code&gt;Cancellation Rate&lt;/code&gt; measure for the X-axis, the &lt;code&gt;Total Revenue&lt;/code&gt; measure for the Y-axis, and &lt;code&gt;customer_id&lt;/code&gt; from &lt;code&gt;dim_customer&lt;/code&gt; for the value.&lt;/p&gt;

&lt;p&gt;Customers with high revenues and low cancellation rates appear toward the upper-left.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 Time-based Cancellation Analysis
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Which periods experience the highest number of cancellations?
&lt;/h4&gt;

&lt;p&gt;To answer this time-based question, I use a clustered column chart as shown in Figure 13.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1r56hm15pts0ghslgjfz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1r56hm15pts0ghslgjfz.png" alt="Figure 13 represents cancelled transactions by month." width="701" height="443"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 13. Clustered column chart showcasing cancelled transactions by month.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I choose the &lt;code&gt;Cancelled transactions&lt;/code&gt; measure for the Y-axis, and &lt;code&gt;i_month&lt;/code&gt; from &lt;code&gt;dim_date&lt;/code&gt; for the X-axis.&lt;/p&gt;

&lt;h4&gt;
  
  
  How does the cancellation rate vary throughout the year?
&lt;/h4&gt;

&lt;p&gt;I create a line chart to visualize the fluctuation of the cancellation rate throughout the year. Figure 14 shows the resulting visualization.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fju9fnf4l3ku3snauwmhq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fju9fnf4l3ku3snauwmhq.png" alt="Figure 14 represents cancelled transactions throughout the year." width="738" height="446"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 14. Line chart showcasing monthly variation in the cancellation rate.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I select &lt;code&gt;Cancellation Rate&lt;/code&gt; for the Y-axis, and &lt;code&gt;i_month&lt;/code&gt; from &lt;code&gt;dim_date&lt;/code&gt; for the X-axis.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Observation:&lt;/u&gt;&lt;/p&gt;

&lt;p&gt;When the number of cancelled transactions and the cancellation rate are compared, it becomes clear that a higher number of cancellations does not always translate into a higher cancellation rate. &lt;/p&gt;

&lt;p&gt;For instance, November's cancellation rate is 1% compared to January's 2%, and there are 1,073 cancelled transactions compared to 700 in January. &lt;/p&gt;

&lt;p&gt;The reason for this discrepancy is that the cancellation rate takes into consideration the total number of transactions that took place during each period.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 Product Cancellation Analysis
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Which products are most frequently associated with cancellations?
&lt;/h4&gt;

&lt;p&gt;I select a treemap to visualize the products that are frequently associated with cancellations as shown in Figure 15.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxqja6v1veb83cui3vqit.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxqja6v1veb83cui3vqit.png" alt="Figure 15 represents products most associated with cancellations." width="766" height="414"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 15. Treemap visualization of top ten products ranked by number of cancelled transactions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I select &lt;code&gt;Cancelled transactions&lt;/code&gt; for values, and &lt;code&gt;stock_code&lt;/code&gt; from &lt;code&gt;dim_product&lt;/code&gt; for category.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.4 Geographic Cancellation Analysis
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Which countries have the highest cancellation rate, and which have the lowest?
&lt;/h4&gt;

&lt;p&gt;I select the Map to highlight the cancellation rates by country. Figure 16 shows the resulting visualization.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuxjp0m8d4ho7hjro38oq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuxjp0m8d4ho7hjro38oq.png" alt="Figure 16 represents cancellation rate by country." width="576" height="422"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 16. Map showing the cancellation rates by country.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Final Report
&lt;/h3&gt;

&lt;p&gt;Figure 17 shows the final and Power BI report, regrouping different statistics and visualizations developped throughout the analysis.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb6rc5y38wa1ahdu1rif0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb6rc5y38wa1ahdu1rif0.png" alt="Figure 17 shows the final and Power BI report." width="799" height="564"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 17. Complete Power BI report.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;During this exercise, I practiced&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connecting Power BI and Snowflake.&lt;/li&gt;
&lt;li&gt;Importing Snowflake data into Power BI.&lt;/li&gt;
&lt;li&gt;Developing analytical measures with Data Analysis Expressions.&lt;/li&gt;
&lt;li&gt;Creating graphics to examine data, including:

&lt;ul&gt;
&lt;li&gt;Bar Charts- Column charts.&lt;/li&gt;
&lt;li&gt;Scatter plots.&lt;/li&gt;
&lt;li&gt;Line charts.&lt;/li&gt;
&lt;li&gt;TreeMaps&lt;/li&gt;
&lt;li&gt;Maps&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This workshop demonstrated how to utilize Power BI as an analytical and visualization layer on top of a data warehouse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/kitchen_code/desining-and-building-an-online-retail-data-warehouse-with-dbt-and-snowflake-5de7"&gt;Desining and Building an Online Retail Data Warehouse with dbt and Snowflake&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://archive.ics.uci.edu/dataset/352/online+retail" rel="noopener noreferrer"&gt;Online Retail dataset - by Daqing Chen&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>powerbi</category>
      <category>datawarehouse</category>
      <category>datasanalysis</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Desining and Building an Online Retail Data Warehouse with dbt and Snowflake</title>
      <dc:creator>kitchen_code</dc:creator>
      <pubDate>Sun, 16 Aug 2026 04:51:20 +0000</pubDate>
      <link>https://dev.to/kitchen_code/desining-and-building-an-online-retail-data-warehouse-with-dbt-and-snowflake-5de7</link>
      <guid>https://dev.to/kitchen_code/desining-and-building-an-online-retail-data-warehouse-with-dbt-and-snowflake-5de7</guid>
      <description>&lt;p&gt;This article is intended for data engineers with a fundamental grasp of the following principles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python programming.&lt;/li&gt;
&lt;li&gt;SQL querying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract, Load, Transform (ELT)&lt;/strong&gt; pipelines.&lt;/li&gt;
&lt;li&gt;Star schema data modeling.&lt;/li&gt;
&lt;li&gt;Data warehousing.&lt;/li&gt;
&lt;li&gt;Data validation and cleansing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I design and implement a data warehouse following an ELT paradigm with the following technology stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python for data ingestion.&lt;/li&gt;
&lt;li&gt;Snowflake for data storage.&lt;/li&gt;
&lt;li&gt;Dbt for data transformation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The article does not cover pipeline orchestration or other topics outside this scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Online Retail Data Warehouse with dbt and Snowflake
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Implementation Roadmap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The following roadmap outlines the implementation process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Data warehouse
├── Dataset analysis
│   ├── Analytical requirements
│   ├── Data quality requirements
│   └── Define the grain
├── Data modeling
│   ├── Conceptual model
│   ├── Logical model
│   └── Physical model
├── Architecture overview
├── Data ingestion
├── dbt setup
│   ├── Environment setup
│   └── Create project and connect to Snowflake
└── Data quality management
    ├── Staging
    │   ├── Data validation
    │   │   ├── Data type check
    │   │   └── Data uniqueness check
    │   └── Data cleansing
    │       └── Type cast and Column rename
    ├── Intermediate
    │   ├── Data validation
    │   │   ├── Range check
    │   │   └── Format check
    │   └── Data cleansing
    │       ├── Deduplication
    │       ├── Missing values
    │       └── Derived fields
    └── Marts
        ├── Implement dimensions
        └── Implement fact table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dataset Analysis
&lt;/h3&gt;

&lt;p&gt;I use the &lt;a href="https://archive.ics.uci.edu/dataset/352/online+retail" rel="noopener noreferrer"&gt;Online Retail&lt;/a&gt; dataset by Daqing Chen, made available through the &lt;a href="https://archive.ics.uci.edu/" rel="noopener noreferrer"&gt;UCI Machine Learning Repository&lt;/a&gt; under the &lt;a href="https://creativecommons.org/licenses/by/4.0/legalcode" rel="noopener noreferrer"&gt;CC BY 4.0&lt;/a&gt; license. &lt;/p&gt;

&lt;p&gt;This dataset represents transactions from a UK-based and registered non-store online retailer. The dataset comprises 541,909 records, recorded between 1 December 2010 and 9 December 2011.&lt;/p&gt;

&lt;p&gt;The dataset contains the following features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;InvoiceNo: Six-digit numbers that uniquely identify transactions; codes starting with &lt;code&gt;C&lt;/code&gt; indicate cancellations.&lt;/li&gt;
&lt;li&gt;StockCode: Codes that uniquely identify products.&lt;/li&gt;
&lt;li&gt;Description: Product names.&lt;/li&gt;
&lt;li&gt;Quantity: Quantity of a product in each transaction.&lt;/li&gt;
&lt;li&gt;InvoiceDate: Date and time when the transaction was generated.&lt;/li&gt;
&lt;li&gt;UnitPrice: Product price per unit.&lt;/li&gt;
&lt;li&gt;CustomerID: Five-digit numbers that uniquely identify customers.&lt;/li&gt;
&lt;li&gt;Country: Country where the customers reside.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Analytical requirements
&lt;/h4&gt;

&lt;p&gt;An analysis of the dataset revealed several opportunities that can be addressed through a structured data warehouse. These opportunities are primarily centered around cancelled transactions. Based on these observations, I outline the following analytical questions that the data warehouse should answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which customers generate the most cancelled transactions?&lt;/li&gt;
&lt;li&gt;Which customers generate the most transactions while having the lowest cancellation rate? &lt;/li&gt;
&lt;li&gt;Which customers generate the most revenue?&lt;/li&gt;
&lt;li&gt;Which customers purchase the largest quantity of products?&lt;/li&gt;
&lt;li&gt;Which customers purchase products with the highest average price per unit? &lt;/li&gt;
&lt;li&gt;Which customers generate high revenue while maintaining a low cancellation rate? &lt;/li&gt;
&lt;li&gt;Which periods experience the highest number of cancellations? &lt;/li&gt;
&lt;li&gt;How does the cancellation rate vary throughout the year?&lt;/li&gt;
&lt;li&gt;Which products are most frequently associated with cancellations? &lt;/li&gt;
&lt;li&gt;Which countries have the highest cancellation rate, and which have the lowest? &lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Data quality requirements
&lt;/h4&gt;

&lt;p&gt;This analysis also surfaced the following data quality issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing values.&lt;/li&gt;
&lt;li&gt;Negative unit prices.&lt;/li&gt;
&lt;li&gt;Duplicate records.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Negative quantities may represent cancelled/returned transactions rather than ordinary sales. Negative unit price, on the other hand, may indicate anomalous values requiring further investigation.&lt;/p&gt;

&lt;h4&gt;
  
  
  Define the grain
&lt;/h4&gt;

&lt;p&gt;The examination of the dataset indicates that a single transaction can span multiple rows since a transaction may contain multiple products.&lt;/p&gt;

&lt;p&gt;Figure 1 provides an example from the dataset.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8j8op6ffavpy0iro5823.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8j8op6ffavpy0iro5823.png" alt="Figure 1 represents an example of a transaction within the Online Retail dataset" width="800" height="375"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1. Example of a transaction within the Online Retail dataset&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Grain: One row represents one transaction record for a product and customer.&lt;/p&gt;
&lt;h3&gt;
  
  
  Data modeling
&lt;/h3&gt;

&lt;p&gt;Data modeling is the translation of business requirements into organized data structures that support analysis. I design the data model following the three phases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conceptual model.&lt;/li&gt;
&lt;li&gt;Logical model.&lt;/li&gt;
&lt;li&gt;Physical model.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Conceptual model
&lt;/h4&gt;

&lt;p&gt;The conceptual data model provides a high-level representation of the main business entities and the relationships between them. I do not implement details such as columns, data types, and database-specific structures at this stage.&lt;/p&gt;

&lt;p&gt;The main entities identified from the analytical requirements are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transaction.&lt;/li&gt;
&lt;li&gt;Product.&lt;/li&gt;
&lt;li&gt;Customer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Country&lt;/code&gt; is considered an attribute of Customer and not as a separate entity.&lt;/p&gt;
&lt;h4&gt;
  
  
  Logical model
&lt;/h4&gt;

&lt;p&gt;The logical model offers a detailed description of the data and expands on the conceptual model.&lt;/p&gt;

&lt;p&gt;At this stage, I choose the star schema for the data warehouse.&lt;/p&gt;

&lt;p&gt;The fact table is connected to dimensions using foreign keys and is depicted in the center. &lt;/p&gt;
&lt;h5&gt;
  
  
  Dimensions
&lt;/h5&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dim_customer&lt;/code&gt; (Primary key: &lt;code&gt;customer_key&lt;/code&gt;, surrogate) - &lt;code&gt;customer_id&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dim_product&lt;/code&gt; (Primary key: &lt;code&gt;product_key&lt;/code&gt;, surrogate) - &lt;code&gt;stock_code&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dim_date&lt;/code&gt; (Primary key: date_key, surrogate) - &lt;code&gt;invoice_date&lt;/code&gt;, &lt;code&gt;i_year&lt;/code&gt;, &lt;code&gt;i_month&lt;/code&gt;, &lt;code&gt;i_day&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;
  
  
  Fact table: &lt;code&gt;fact_transaction&lt;/code&gt;
&lt;/h5&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Grain: One row represents one transaction record for a product and customer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Primary key: &lt;code&gt;transaction_key&lt;/code&gt;, surrogate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Measures: &lt;code&gt;quantity&lt;/code&gt;, &lt;code&gt;unit_price&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Degenerate key: &lt;code&gt;invoice_id&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Foreign keys: &lt;code&gt;customer_key&lt;/code&gt;, &lt;code&gt;product_key&lt;/code&gt;, &lt;code&gt;date_key&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Derived fields: &lt;code&gt;is_cancelled&lt;/code&gt; (determines whether the transaction is cancelled or not). &lt;code&gt;total_cost&lt;/code&gt; (multiply &lt;code&gt;quantity&lt;/code&gt; by absolute &lt;code&gt;unit_price&lt;/code&gt;). &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;
  
  
  Relationships
&lt;/h5&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dim_customer&lt;/code&gt; 1 → N &lt;code&gt;fact_transaction&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dim_product&lt;/code&gt; 1 → N &lt;code&gt;fact_transaction&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dim_date&lt;/code&gt; 1 → N &lt;code&gt;fact_transaction&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  Physical model
&lt;/h4&gt;

&lt;p&gt;The physical model represents the implementation of the data structure in a specific database management system. In this project, I choose Snowflake as the data warehouse. &lt;/p&gt;

&lt;p&gt;Figure 2 illustrates the physical implementation of the data warehouse schema.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7payiy53j511ja5yap57.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7payiy53j511ja5yap57.png" alt="Figure 2 represents the physical implementation of the data warehouse schema on Snowflake" width="800" height="642"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2. Physical model of the Online Retail data warehouse in Snowflake.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Architecture overview
&lt;/h3&gt;

&lt;p&gt;Figure 3 illustrates the pipeline's system architecture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpzaud2q05qguo8o3oey7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpzaud2q05qguo8o3oey7.png" alt="Online Retail pipeline's architecture" width="682" height="491"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3. Online retail data warehouse pipeline architecture. A Python application (extract and load modules, using &lt;code&gt;snowflake-connector-python&lt;/code&gt;) is used to extract and load raw sales data (&lt;code&gt;online_retail.xlsx&lt;/code&gt;) into Snowflake. The staging, intermediate, and marts layers of a staged pipeline are then used by dbt to modify the raw data, creating tables in the Snowflake data warehouse (&lt;code&gt;online_retail_warehouse&lt;/code&gt;) that are ready for analytics.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Data ingestion
&lt;/h3&gt;

&lt;p&gt;I create a Python application for the data ingestion layer. The following tree represents the project structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;online_retail_ingestion/
│
├── data/
│   └── online_retail.xlsx
│
├── workflow/
│   ├── __init__.py
│   ├── extract.py
│   └── load.py
│
├── main.py
├── requirements.txt
├── .gitignore
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dataset is stored in the &lt;code&gt;data&lt;/code&gt; folder, while the extraction and loading stages are implemented in separate modules. &lt;/p&gt;

&lt;p&gt;The application workflow consists of three steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extract the data from the &lt;code&gt;.xlsx&lt;/code&gt; file using the &lt;code&gt;OpenPyXL&lt;/code&gt; library.&lt;/li&gt;
&lt;li&gt;Establish a connection to Snowflake.&lt;/li&gt;
&lt;li&gt;Load raw data into a Snowflake table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See &lt;a href="https://github.com/k1ssa1/python_snowflake_online_retail_data_ingestion" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; for full implementation.&lt;/p&gt;

&lt;h4&gt;
  
  
  Connect to Snowflake
&lt;/h4&gt;

&lt;p&gt;To connect the Python application to Snowflake, I need the following configuration values: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Warehouse.&lt;/li&gt;
&lt;li&gt;Database.&lt;/li&gt;
&lt;li&gt;Schema.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First, I created a Snowflake free trial account, and selected Google Cloud Platform as the cloud data storage for my Snowflake environment.&lt;/p&gt;

&lt;p&gt;Using the Snowflake web interface Snowsight, I navigate to the workspace and create a new SQL file. I use this file to provision the resources required by the ingestion layer. The following SQL statements create the warehouse, the database, and the schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="n"&gt;online_retail_warehouse&lt;/span&gt;
&lt;span class="n"&gt;WAREHOUSE_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'XSMALL'&lt;/span&gt;
&lt;span class="n"&gt;AUTO_SUSPEND&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 

&lt;span class="n"&gt;USE&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="n"&gt;online_retail_warehouse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;online_retail_db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;USE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;online_retail_db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;retail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I set the size of the warehouse. &lt;code&gt;AUTO_SUSPEND = 900&lt;/code&gt; suspends the warehouse after 900 seconds (15 minutes) of inactivity.&lt;/p&gt;

&lt;p&gt;Once I specify these configuration values, I use the &lt;code&gt;snowflake-connector-python&lt;/code&gt; library to establish the connection. I load the raw data inside the &lt;code&gt;invoices_raw_data&lt;/code&gt; table in the &lt;code&gt;retail&lt;/code&gt; schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  dbt setup
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Environment setup
&lt;/h4&gt;

&lt;p&gt;Connecting dbt to Snowflake requires a set of configuration values.&lt;/p&gt;

&lt;p&gt;In Snowsight, I navigate to the SQL file in the workspace and execute the following SQL statements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;dbt_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="n"&gt;online_retail_warehouse&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;dbt_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;online_retail_db&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;dbt_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;online_retail_db&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;dbt_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;ONLINE_RETAIL_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RETAIL&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;dbt_role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;your_username&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'your_password'&lt;/span&gt;
    &lt;span class="n"&gt;DEFAULT_ROLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dbt_role&lt;/span&gt;
    &lt;span class="n"&gt;DEFAULT_WAREHOUSE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;online_retail_warehouse&lt;/span&gt;
    &lt;span class="n"&gt;DEFAULT_NAMESPACE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ONLINE_RETAIL_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RETAIL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;dbt_role&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;your_username&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a separate Python virtual environment, I install &lt;code&gt;dbt-core&lt;/code&gt; and &lt;code&gt;dbt-snowflake&lt;/code&gt; using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;dbt-core dbt-snowflake
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the packages are installed, I create the &lt;code&gt;profiles.yml&lt;/code&gt; file. This YAML configuration file defines the Snowflake profile used by dbt. &lt;/p&gt;

&lt;p&gt;On Windows, I navigate to the user folder and create the &lt;code&gt;.dbt&lt;/code&gt; directory. Inside this directory, I create &lt;code&gt;profiles.yml&lt;/code&gt; as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;snowflake-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;

  &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;snowflake&lt;/span&gt;
      &lt;span class="na"&gt;account&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;your Snowflake account identifier&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;username chosen in Snowflake&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;password chosen in Snowflake&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dbt_role&lt;/span&gt;
      &lt;span class="na"&gt;warehouse&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;online_retail_warehouse&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;online_retail_db&lt;/span&gt;
      &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RETAIL&lt;/span&gt;
      &lt;span class="na"&gt;threads&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
      &lt;span class="na"&gt;client_session_keep_alive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;False&lt;/span&gt;
      &lt;span class="na"&gt;query_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;define a query tag&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;dbt_online_retail&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Create project and connect to Snowflake
&lt;/h4&gt;

&lt;p&gt;Once the configuration requirements are complete, I initialize the dbt project inside the Python virtual environment. The following command initializes the dbt project using the &lt;code&gt;snowflake-db&lt;/code&gt; profile.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dbt init &lt;span class="nt"&gt;--profile&lt;/span&gt; snowflake-db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command prompts me to submit the project name. In this case, the project name is &lt;code&gt;online_retail_dbt&lt;/code&gt;. The command creates the standard dbt project scaffold.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://github.com/k1ssa1/online-retail-dbt" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; for the full implementation of the transformation layer.&lt;/p&gt;

&lt;p&gt;Inside &lt;code&gt;dbt_project.yml&lt;/code&gt;, I set the &lt;code&gt;profile&lt;/code&gt; value to &lt;code&gt;snowflake-db&lt;/code&gt; which references the profile created in &lt;code&gt;profiles.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;online_retail_dbt'&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.0.0'&lt;/span&gt;

&lt;span class="c1"&gt;# This setting configures which "profile" dbt uses for this project.&lt;/span&gt;
&lt;span class="na"&gt;profile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;snowflake-db'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After I save the file, I test the connection by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dbt debug &lt;span class="nt"&gt;--connection&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Data quality management
&lt;/h3&gt;

&lt;p&gt;In the &lt;code&gt;dbt_project.yml&lt;/code&gt; file, I define how the models in each transformation layer are materialized in the warehouse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;online_retail_dbt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="na"&gt;staging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;+materialized&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;view&lt;/span&gt;

    &lt;span class="na"&gt;intermediate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;+materialized&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;view&lt;/span&gt;

    &lt;span class="na"&gt;marts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;+materialized&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;table&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Staging
&lt;/h4&gt;

&lt;p&gt;I create the &lt;code&gt;staging&lt;/code&gt; subdirectory in the &lt;code&gt;models&lt;/code&gt; directory. Next, I add the &lt;code&gt;sources.yml&lt;/code&gt; file. This file declares the raw Snowflake table as a dbt source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;

&lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;raw&lt;/span&gt;
    &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ONLINE_RETAIL_DB&lt;/span&gt;
    &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RETAIL&lt;/span&gt;

    &lt;span class="na"&gt;tables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;invoices_raw_data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I create the &lt;code&gt;schema.yml&lt;/code&gt; to document the staging model and its columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;

&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stg_online_retail&lt;/span&gt;

      &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;invoice_id&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stock_code&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;description&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;quantity&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;invoice_date&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unit_price&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;country&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Data validation
&lt;/h5&gt;

&lt;p&gt;The data validation files are created inside the &lt;code&gt;tests&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;At this stage, I perform the following checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dq_data_type_check.sql&lt;/code&gt;: validates the data types of all the columns in the &lt;code&gt;invoices_raw_data&lt;/code&gt; table.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dq_uniqueness_check.sql&lt;/code&gt;: validates the uniqueness of the combination of (&lt;code&gt;INVOICENO&lt;/code&gt;, &lt;code&gt;STOCKCODE&lt;/code&gt; and &lt;code&gt;CUSTOMERID&lt;/code&gt;) in the &lt;code&gt;invoices_raw_data&lt;/code&gt; table.&lt;/li&gt;
&lt;/ul&gt;

&lt;h6&gt;
  
  
  Data type check
&lt;/h6&gt;

&lt;p&gt;I create the &lt;code&gt;dq_data_type_check.sql&lt;/code&gt; file as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="n"&gt;expected_types&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
      &lt;span class="s1"&gt;'INVOICENO'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;COLUMN_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'VARCHAR'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;EXPECTED_DATA_TYPE&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
      &lt;span class="s1"&gt;'STOCKCODE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'VARCHAR'&lt;/span&gt;
    &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
      &lt;span class="s1"&gt;'DESCRIPTION'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'VARCHAR'&lt;/span&gt;
    &lt;span class="c1"&gt;-- Define the expected data type for each column in the raw table.&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;actual_types&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
      &lt;span class="k"&gt;COLUMN_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;DATA_TYPE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ACTUAL_DATA_TYPE&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;
      &lt;span class="n"&gt;INFORMATION_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;
      &lt;span class="n"&gt;TABLE_SCHEMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'RETAIL'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'INVOICES_RAW_DATA'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;COLUMN_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EXPECTED_DATA_TYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ACTUAL_DATA_TYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;CASE&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ACTUAL_DATA_TYPE&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'MISSING'&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EXPECTED_DATA_TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ACTUAL_DATA_TYPE&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'VALIDATED'&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EXPECTED_DATA_TYPE&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ACTUAL_DATA_TYPE&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'TYPE MISMATCH'&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="n"&gt;expected_types&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
  &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;actual_types&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;COLUMN_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;COLUMN_NAME&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ACTUAL_DATA_TYPE&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
  &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EXPECTED_DATA_TYPE&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ACTUAL_DATA_TYPE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query checks the data types of the columns in the &lt;code&gt;invoices_raw_data&lt;/code&gt; table. I specify the expected data types based on the physical model and compare them against the data types in the corresponding columns in &lt;code&gt;invoices_raw_data&lt;/code&gt;.&lt;/p&gt;

&lt;h6&gt;
  
  
  Uniqueness check
&lt;/h6&gt;

&lt;p&gt;I add the &lt;code&gt;dq_uniqueness_check.sql&lt;/code&gt; file as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;INVOICENO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;STOCKCODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CUSTOMERID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;occurrence_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"raw"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'invoices_raw_data'&lt;/span&gt;&lt;span class="p"&gt;)}}&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;INVOICENO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;STOCKCODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CUSTOMERID&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test verifies the occurrence of the combination of &lt;code&gt;INVOICENO&lt;/code&gt;, &lt;code&gt;STOCKCODE&lt;/code&gt; and &lt;code&gt;CUSTOMERID&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To run the tests, I use the command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dbt &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--select&lt;/span&gt; &amp;lt;file name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The uniqueness check indicates that the combination of &lt;code&gt;INVOICENO&lt;/code&gt;, &lt;code&gt;STOCKCODE&lt;/code&gt;, and &lt;code&gt;CUSTOMERID&lt;/code&gt; is not unique. Further investigation showed that repeated combinations can contain different attributes such as quantities or unit prices. Therefore, I don't consider these records duplicates, as this may reflect how the source transaction system records item-level transactions.&lt;/p&gt;

&lt;h5&gt;
  
  
  Data cleansing
&lt;/h5&gt;

&lt;h6&gt;
  
  
  Type cast and Column rename
&lt;/h6&gt;

&lt;p&gt;Inside the staging directory, I create the &lt;code&gt;stg_online_retail.sql&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INVOICENO&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STOCKCODE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stock_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DESCRIPTION&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QUANTITY&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INVOICEDATE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMP_NTZ&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;invoice_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UNITPRICE&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;unit_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CUSTOMERID&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COUNTRY&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'raw'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'invoices_raw_data'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This statement casts each column in &lt;code&gt;invoices_raw_data&lt;/code&gt; to its expected data type, and renames the columns according to the physical model.&lt;/p&gt;

&lt;h4&gt;
  
  
  Intermediate
&lt;/h4&gt;

&lt;h5&gt;
  
  
  Data validation
&lt;/h5&gt;

&lt;h6&gt;
  
  
  Range check
&lt;/h6&gt;

&lt;p&gt;I create &lt;code&gt;dq_range_check.sql&lt;/code&gt; inside the &lt;code&gt;tests&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;staged_data&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stg_online_retail'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;range_validation&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;staged_data&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;invoice_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2011-12-10'&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;invoice_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2010-12-01'&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;unit_price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;range_validation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test indicates the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Negative quantities are only associated with canceled transactions. Therefore, I don't consider this as a quality issue. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Negative unit prices indicate a quality issue that needs to be addressed. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All invoices dates fall within the expected date range. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h6&gt;
  
  
  Format check
&lt;/h6&gt;

&lt;p&gt;I create &lt;code&gt;dq_format_check.sql&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;staged_data&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stg_online_retail'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;format_validation&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unit_price&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;staged_data&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;REGEXP_LIKE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'^(C|[0-9]).*'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;REGEXP_LIKE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'^-?[0-9]+(&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s1"&gt;[0-9]+)?$'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;REGEXP_LIKE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'^-?[0-9]+$'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;format_validation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test result indicates three invoice IDs beginning with the letter 'A'. However, these records represent bad-debt adjustments rather than standard retail transactions. Therefore, I exclude them for the next stage.&lt;/p&gt;

&lt;h5&gt;
  
  
  Data cleansing
&lt;/h5&gt;

&lt;p&gt;Inside the intermediate directory, I create the schema.yml as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;

&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;int_online_retail&lt;/span&gt;

      &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;invoice_id&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stock_code&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;description&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;quantity&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;invoice_date&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unit_price&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;country&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_cost&lt;/span&gt;

        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;is_cancelled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, I create &lt;code&gt;int_online_retail.sql&lt;/code&gt; to create the view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="n"&gt;staged_data&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
      &lt;span class="o"&gt;*&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;
      &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stg_online_retail'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;clean_data&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt;
      &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;stock_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Unknown'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;invoice_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;ABS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;unit_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Unknown'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;ABS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;invoice_id&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'C%'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
      &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;is_cancelled&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;
      &lt;span class="n"&gt;staged_data&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt;
      &lt;span class="n"&gt;invoice_id&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'A%'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="n"&gt;clean_data&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These SQL statements perform the following actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deduplication: remove only identical rows across all columns. The result is a table containing 536,638 records.&lt;/li&gt;
&lt;li&gt;Handle missing values: replace missing values with their corresponding placeholder values.&lt;/li&gt;
&lt;li&gt;Create derived fields: Add the &lt;code&gt;total_cost&lt;/code&gt; field by multiplying &lt;code&gt;quantity&lt;/code&gt; by &lt;code&gt;unit_price&lt;/code&gt;. Additionally, negative &lt;code&gt;unit_price&lt;/code&gt; values are converted into positive values. Add the boolean &lt;code&gt;is_cancelled&lt;/code&gt; field that determines if a transaction is cancelled or not.&lt;/li&gt;
&lt;li&gt;Bad-debt adjustments: Exclude records with invoice IDs starting with "A" from the &lt;code&gt;int_online_retail&lt;/code&gt; view.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Marts
&lt;/h4&gt;

&lt;p&gt;At this final stage, I implement the star schema using &lt;code&gt;int_online_retail&lt;/code&gt;.&lt;/p&gt;

&lt;h5&gt;
  
  
  Implement dimensions
&lt;/h5&gt;

&lt;p&gt;I create the following dimensions each with its own surrogate key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dim_customer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dim_product&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dim_date&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;dim_customer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;clean_data&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'int_online_retail'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;customer_countries&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;country_count&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;clean_data&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
        &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;country&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;dim_customer_nsk&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;country&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customer_countries&lt;/span&gt;
    &lt;span class="n"&gt;QUALIFY&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
        &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;country_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;country&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dim_customer_nsk&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since customers can be associated with multiple countries, I select the most frequently occurring country for each customer. This ensures &lt;code&gt;customer_id&lt;/code&gt; is unique in &lt;code&gt;dim_customer&lt;/code&gt; while preserving the dominant country association. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;dim_product&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;clean_data&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'int_online_retail'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;product_descriptions&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;stock_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;description_count&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;clean_data&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;stock_code&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
        &lt;span class="n"&gt;stock_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;dim_product_nsk&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;stock_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;product_descriptions&lt;/span&gt;
    &lt;span class="n"&gt;QUALIFY&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;stock_code&lt;/span&gt;
        &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;description_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;dim_product&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;stock_code&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;product_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stock_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dim_product_nsk&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dim_product&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since &lt;code&gt;stock_code&lt;/code&gt; can be associated with multiple descriptions, I select only the frequently occurring description for each product. This ensures the uniqueness of &lt;code&gt;stock_code&lt;/code&gt; in &lt;code&gt;dim_product&lt;/code&gt; while preserving the most frequent description.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dim_date&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="n"&gt;clean_data&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
      &lt;span class="o"&gt;*&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;
      &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'int_online_retail'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;dim_date_nsk&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
      &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;invoice_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;YEAR&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt;
          &lt;span class="n"&gt;invoice_date&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;i_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;MONTH&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt;
          &lt;span class="n"&gt;invoice_date&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;i_month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;DAY&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt;
          &lt;span class="n"&gt;invoice_date&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;i_day&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;
      &lt;span class="n"&gt;clean_data&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;dim_date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
      &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
          &lt;span class="n"&gt;invoice_date&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;date_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;invoice_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;i_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;i_month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;i_day&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt;
      &lt;span class="n"&gt;dim_date_nsk&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="n"&gt;dim_date&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Implement fact table
&lt;/h5&gt;

&lt;p&gt;I create &lt;code&gt;fact_transaction.sql&lt;/code&gt; inside the marts directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stock_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoice_date&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;transaction_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_cancelled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_cost&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'int_online_retail'&lt;/span&gt;&lt;span class="p"&gt;)}}&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dim_customer'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dim_product'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stock_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stock_code&lt;/span&gt;
    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dim_date'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoice_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoice_date&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I run the following command to build the dimensions and the fact table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dbt run &lt;span class="nt"&gt;--select&lt;/span&gt; dim_customer dim_product dim_date fact_transaction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Validate star schema
&lt;/h5&gt;

&lt;p&gt;Having implemented the star schema, I add the following validation tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test the uniqueness and integrity of the surrogate keys.&lt;/li&gt;
&lt;li&gt;Test the foreign key relationships.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final schema file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;

&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dim_customer&lt;/span&gt;
    &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_key&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unique&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unique&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;country&lt;/span&gt;


  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dim_product&lt;/span&gt;
    &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;product_key&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unique&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stock_code&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unique&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;description&lt;/span&gt;


  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dim_date&lt;/span&gt;
    &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;date_key&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unique&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;invoice_date&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unique&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;i_year&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;i_month&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;i_day&lt;/span&gt;


  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fact_transaction&lt;/span&gt;
    &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;transaction_key&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unique&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_key&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;relationships&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('dim_customer')&lt;/span&gt;
              &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_key&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;product_key&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;relationships&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('dim_product')&lt;/span&gt;
              &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;product_key&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;date_key&lt;/span&gt;
        &lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;not_null&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;relationships&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('dim_date')&lt;/span&gt;
              &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;date_key&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;invoice_id&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;is_cancelled&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;quantity&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unit_price&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_cost&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwrdhrbnj3tait4naem82.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwrdhrbnj3tait4naem82.png" alt="Figure 4 showcases successful creation and population of the star schema on Snowsight" width="800" height="350"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 4. Snowsight showcases the successful creation of the dimensions and the fact table, along with the population of &lt;code&gt;fact_transaction&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/k1ssa1/python_snowflake_online_retail_data_ingestion" rel="noopener noreferrer"&gt;Python ingestion - GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/k1ssa1/online-retail-dbt" rel="noopener noreferrer"&gt;Data transformation with dbt - GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://archive.ics.uci.edu/dataset/352/online+retail" rel="noopener noreferrer"&gt;Online Retail dataset - by Daqing Chen&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trademarks&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python is a registered trademark of the Python Software Foundation.&lt;/li&gt;
&lt;li&gt;Snowflake is a registered trademark of Snowflake Inc.&lt;/li&gt;
&lt;li&gt;dbt and dbt-core are registered trademarks of dbt Labs, Inc. &lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>datawarehouse</category>
      <category>python</category>
      <category>snowflake</category>
      <category>dbt</category>
    </item>
    <item>
      <title>Orchestrate ETL Pipelines With Apache Airflow®</title>
      <dc:creator>kitchen_code</dc:creator>
      <pubDate>Mon, 10 Aug 2026 02:28:48 +0000</pubDate>
      <link>https://dev.to/kitchen_code/orchestrate-etl-pipelines-with-apache-airflowr-5eio</link>
      <guid>https://dev.to/kitchen_code/orchestrate-etl-pipelines-with-apache-airflowr-5eio</guid>
      <description>&lt;p&gt;This document demonstrates a simple &lt;strong&gt;extract, transform, and load (ETL)&lt;/strong&gt; pipeline orchestration with Apache Airflow. &lt;/p&gt;

&lt;p&gt;The document is intended for data engineers who have a basic understanding of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ETL processes.&lt;/li&gt;
&lt;li&gt;Python / pandas.&lt;/li&gt;
&lt;li&gt;PostgreSQL®.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows Subsystem for Linux (WSL 2)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Ubuntu.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transmission Control Protocol and Internet Protocol (TCP/IP)&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The document focuses on orchestration and does not explain the implementation of the ETL processes. &lt;/p&gt;

&lt;h2&gt;
  
  
  Open-Meteo Weather API ETL pipeline orchestration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem Definition
&lt;/h3&gt;

&lt;p&gt;In my previous case studies, I built data pipelines that required manual execution. The absence of orchestration introduces repetitive tasks, and limits scalability. As the system grows, and data processing requirements increase, manual execution becomes impractical, and leads to operational delays.&lt;/p&gt;

&lt;p&gt;In this case study, I orchestrate the &lt;a href="https://open-meteo.com/" rel="noopener noreferrer"&gt;Open-Meteo Weather API&lt;/a&gt; ETL pipeline. API data are offered under &lt;a href="https://creativecommons.org/licenses/by/4.0/deed.en" rel="noopener noreferrer"&gt;Attribution 4.0 International (CC BY 4.0)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This forecast ETL pipeline monitors daily Weather variables for the Casablanca metropolitan area.&lt;/p&gt;

&lt;p&gt;The data pipeline workflow is defined as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The pipeline retrieves a selected subset of Weather variables from the API using predefined parameters.&lt;/li&gt;
&lt;li&gt;The pipeline flattens the required nested objects from the JSON response into DataFrames. This process generates two DataFrames. The first one stores daily Weather data for the area at two-hour intervals. The second one tracks the daily measurement units and their insertion date. &lt;/li&gt;
&lt;li&gt;The pipeline loads the resulting data into two distinct relational tables in a PostgreSQL database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the full implementation of the ETL processes, see &lt;a href="https://github.com/k1ssa1/Apache-Airflow-ETL-Pipeline-With-Open-Meteo-API-" rel="noopener noreferrer"&gt;the GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem Solution
&lt;/h3&gt;

&lt;p&gt;Modern data engineering offers a wide range of orchestration tools for batch processing and stream processing. &lt;/p&gt;

&lt;p&gt;Apache Airflow® (or simply Airflow) is an open-source orchestration platform. It enables users to write, schedule, and monitor data workflows. Airflow supports batch processing, which makes the platform suitable for orchestrating the Open-Meteo Weather API ETL pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Execution Environment
&lt;/h4&gt;

&lt;p&gt;Figure 1 provides an overview of the pipeline architecture and the Airflow orchestration implementation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkqptmynhffr7wngj39pm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkqptmynhffr7wngj39pm.png" alt="Figure 1 provides an overview of the pipeline architecture and the Airflow orchestration implementation." width="800" height="513"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1. Overview of the Open-Meteo API ETL pipeline architecture and the implementation of the Airflow orchestration.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;WSL 2 provides an Ubuntu environment for installing and running Airflow, limiting the risk of compatibility issues. The Python project also runs inside WSL 2, while the PostgreSQL database runs locally on the host Windows operating system.&lt;/p&gt;
&lt;h4&gt;
  
  
  Airflow Installation
&lt;/h4&gt;

&lt;p&gt;In this case study, I installed Airflow in two environments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A main Airflow installation in the Ubuntu WSL 2 environment.&lt;/li&gt;
&lt;li&gt;An Airflow installation inside the Python application's virtual environment using &lt;code&gt;pip&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main Airflow installation is responsible for the Airflow environment and its associated configuration, including the metadata database, administrator credentials, and other Airflow settings.&lt;/p&gt;

&lt;p&gt;The installation inside the Python application's virtual environment provides the Airflow packages required by the project and allows Airflow to run within the same Python environment as the application's dependencies.&lt;/p&gt;

&lt;p&gt;This distinction matters later when running &lt;code&gt;Airflow standalone&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For further information on installing Airflow on WSL 2, see the &lt;a href="https://airflow.apache.org/docs/apache-airflow/stable/start.html" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;
  
  
  Airflow logic
&lt;/h4&gt;

&lt;p&gt;Airflow manages the workflow execution, while the Python application executes the ETL processes. &lt;/p&gt;

&lt;p&gt;In Airflow, a &lt;strong&gt;Directed Acyclic Graph (DAG)&lt;/strong&gt; is a file that defines the structure of workflows. A DAG contains all the &lt;strong&gt;tasks&lt;/strong&gt; and their &lt;strong&gt;dependencies&lt;/strong&gt;, which determine the order in which Airflow orchestrates the pipeline.&lt;/p&gt;

&lt;p&gt;In order to orchestrate this ETL pipeline with Airflow, we define the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a DAG file.&lt;/li&gt;
&lt;li&gt;Define the tasks and dependencies.&lt;/li&gt;
&lt;li&gt;Configure the TCP/IP connection between the Python application and the PostgreSQL database.&lt;/li&gt;
&lt;li&gt;Verify the creation of the DAG using the Airflow standalone GUI.&lt;/li&gt;
&lt;li&gt;Monitor the data loading in the PostgreSQL database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;1. Create the DAG file&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Airflow project uses the &lt;code&gt;airflow/dags&lt;/code&gt; directory in the root of the Python project. The &lt;code&gt;dags&lt;/code&gt; subfolder contains all the DAG files that Airflow can use. In this case, I define a single file.&lt;/p&gt;

&lt;p&gt;The project structure is as follows:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;open_meteo_airflow_pipeline/&lt;br&gt;
│&lt;br&gt;
├── airflow/&lt;br&gt;
│   └── dags/&lt;br&gt;
├── pipeline/&lt;br&gt;
│   ├── extract.py&lt;br&gt;
│   ├── transform.py&lt;br&gt;
│   └── load.py&lt;br&gt;
├── main.py&lt;br&gt;
├── .env&lt;br&gt;
├── requirements.txt&lt;br&gt;
└── README.md&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Inside &lt;code&gt;dags&lt;/code&gt;, I create the &lt;code&gt;open_meteo_etl_dag.py&lt;/code&gt; DAG file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Define the tasks and dependencies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The following code is defined inside &lt;code&gt;open_meteo_etl_dag.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abspath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;../..&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pipeline.extract&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fetch_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pipeline.transform&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pipeline.load&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_data&lt;/span&gt;

&lt;span class="n"&gt;default_args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kitchen_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry_delay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;execution_timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;etl_dag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;default_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;default_args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;catchup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_task&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transform_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extracted_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hourly_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extracted_data&lt;/span&gt;
        &lt;span class="n"&gt;units_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weather_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;hourly_data&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;units_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/weather_units.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;weather_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/weather_data.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="n"&gt;units_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;units_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;weather_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weather_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;units_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weather_path&lt;/span&gt;


    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;units_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weather_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;
        &lt;span class="n"&gt;units_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;units_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;weather_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weather_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;load_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;units_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weather_df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


    &lt;span class="n"&gt;extracted_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_task&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;transformed_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extracted_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;load_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transformed_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, I import the Airflow components to define the DAG and the tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;airflow.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DAG constructor accepts a wide range of parameters. In this file, I define the following parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dag_id&lt;/code&gt;: defines the unique identifier that Airflow uses to identify the DAG.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;start_date&lt;/code&gt;: defines the date from which the DAG's scheduled runs should begin.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;schedule&lt;/code&gt;: defines the frequency at which Airflow schedules the DAG. In this project, the DAG is configured to run once per day using &lt;code&gt;@daily&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;default_args&lt;/code&gt;: defines a Python dictionary containing default configuration parameters. In this project, it contains: &lt;code&gt;owner&lt;/code&gt;: identifies the owner responsible for this DAG. &lt;code&gt;retries&lt;/code&gt;: defines the number of times Airflow should retry a task after a failure. &lt;code&gt;retry_delay&lt;/code&gt;: defines the amount of time Airflow should wait before retrying to run a task. &lt;code&gt;execution_timeout&lt;/code&gt;: defines the maximum amount of time a task is allowed to run before Airflow stops it due to a timeout. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;catchup&lt;/code&gt;: tells a DAG whether to run all missed time intervals that occurred between its start_date and the current time. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once I define the DAG parameters, I create the tasks. In this context, we need three tasks: one for each ETL process. &lt;/p&gt;

&lt;p&gt;I use the &lt;code&gt;@task&lt;/code&gt; decorator to create the first task, &lt;code&gt;extract_task&lt;/code&gt;, for the extraction process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_task&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I import the function &lt;code&gt;fetch_data()&lt;/code&gt; from the &lt;code&gt;extract.py&lt;/code&gt; module, and I return the result generated by the function from the task. &lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;@task&lt;/code&gt;, I create the second task &lt;code&gt;transform_task&lt;/code&gt; for the transformation process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt; &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transform_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extracted_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hourly_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extracted_data&lt;/span&gt;
        &lt;span class="n"&gt;units_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weather_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;hourly_data&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;units_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/weather_units.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;weather_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/weather_data.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="n"&gt;units_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;units_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;weather_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weather_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;units_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weather_path&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tasks are connected as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;extracted_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_task&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;transformed_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extracted_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since the tasks were created using the &lt;code&gt;@task&lt;/code&gt; decorator, Airflow automatically handles the communication between them using the &lt;code&gt;XComs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Unlike the traditional approach, where XComs are managed with &lt;code&gt;xcom_push()&lt;/code&gt; and &lt;code&gt;xcom_pull()&lt;/code&gt;, the &lt;a href="https://airflow.apache.org/docs/apache-airflow/stable/tutorial/taskflow.html" rel="noopener noreferrer"&gt;TaskFlow API&lt;/a&gt; handles this process automatically through task inputs and outputs. This defines the &lt;strong&gt;dependencies&lt;/strong&gt; between tasks.  &lt;/p&gt;

&lt;p&gt;The returned values from &lt;code&gt;extract_task&lt;/code&gt; are made available to &lt;code&gt;transform_task&lt;/code&gt; through the &lt;code&gt;extracted_data&lt;/code&gt; parameter. The &lt;code&gt;transform_task&lt;/code&gt; is defined as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The task receives the values returned by &lt;code&gt;fetch_data()&lt;/code&gt;: &lt;code&gt;units&lt;/code&gt;, &lt;code&gt;latitude&lt;/code&gt;, &lt;code&gt;longitude&lt;/code&gt;, &lt;code&gt;hourly_data&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The task imports the &lt;code&gt;transform_data()&lt;/code&gt; function from the &lt;code&gt;transform.py&lt;/code&gt; module and passes the returned values of &lt;code&gt;fetch_data()&lt;/code&gt; to it. The end result is two DataFrames: &lt;code&gt;units_df&lt;/code&gt; and &lt;code&gt;weather_df&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The task converts the two DataFrames into CSV files. This approach allows the next task to access the transformed data without passing the entire DataFrames through XCom. At last, the task returns the path of each file. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Using &lt;code&gt;@task&lt;/code&gt;, I create the last task &lt;code&gt;load_task&lt;/code&gt;, for the loading process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="nd"&gt;@task&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;units_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weather_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;
        &lt;span class="n"&gt;units_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;units_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;weather_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weather_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;load_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;units_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weather_df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tasks are connected as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;transformed_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extracted_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;load_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transformed_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;load_task&lt;/code&gt; takes &lt;code&gt;paths&lt;/code&gt; as a parameter. This allows the task to read the CSV files using their paths, and load their contents back into pandas DataFrame. &lt;/p&gt;

&lt;p&gt;The task calls the &lt;code&gt;load_data()&lt;/code&gt; function from the &lt;code&gt;load.py&lt;/code&gt; module. This function executes the following operations: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Establishes a connection to the PostgreSQL database using the &lt;code&gt;psycopg&lt;/code&gt; driver.&lt;/li&gt;
&lt;li&gt;Executes &lt;strong&gt;Data Definition Language&lt;/strong&gt; statements to create the &lt;code&gt;weather_data&lt;/code&gt;, and &lt;code&gt;weather_units&lt;/code&gt; tables.&lt;/li&gt;
&lt;li&gt;Executes &lt;strong&gt;Data Manipulation Language&lt;/strong&gt; statements to insert the transformed data into the tables.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;3. Configure the TCP/IP connection between the Python application and the PostgreSQL database.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Python application runs inside WSL 2, while the PostgreSQL database runs locally on the Windows host operating system. Since the application and the database run in different environments, the connection between them is established by TCP/IP.&lt;/p&gt;

&lt;p&gt;The psycopg driver requires several connection parameters. These parameters are stored in environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DB_NAME&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="py"&gt;DB_USER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="py"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="py"&gt;DB_HOST&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;span class="py"&gt;DB_PORT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my case, the main configuration issue concerns the &lt;code&gt;DB_HOST&lt;/code&gt; parameter. Since the Python application runs inside WSL 2, the PostgreSQL server that runs on the host operating system cannot be reached using the default &lt;code&gt;localhost&lt;/code&gt; address.&lt;/p&gt;

&lt;p&gt;Therefore, I use the IP address of the Windows host as it is accessible from the WSL 2 environment. &lt;/p&gt;

&lt;p&gt;To retrieve this IP address, run the following command in your Windows terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;ipconfig
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the IP address under the vEthernet (WSL (Hyper-V firewall)) network adapter. This is the IP address to use for the &lt;code&gt;DB_HOST&lt;/code&gt; parameter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Other configuration issues can prevent the connection from being established. Verify that PostgreSQL is configured to accept connections from WSL 2. This includes its &lt;code&gt;listen_addresses&lt;/code&gt; and authentication rules inside the &lt;code&gt;pg_hba.conf&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Verify the creation of the DAG using the Airflow standalone GUI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;airflow standalone&lt;/code&gt; CLI command enables users to quickly spin up all core components of Airflow locally and simultaneously. This command also provides access to a web based graphic interface (GUI) that enables users to monitor the DAGs.&lt;/p&gt;

&lt;p&gt;First, the admin credentials are required to access the GUI. To find the credentials: navigate inside the Airflow main installation package and look for either:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;simple_auth_manager_passwords.json.generated&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;or &lt;/p&gt;

&lt;p&gt;&lt;code&gt;standalone_admin_password.txt&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;by running the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;simple_auth_manager_passwords.json.generated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command returns the account credentials to access the web interface.&lt;/p&gt;

&lt;p&gt;Figure 2 illustrates the commands used to run Airflow in standalone mode from WSL 2.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0d2vpt2sde8tknzt7kq7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0d2vpt2sde8tknzt7kq7.png" alt="Figure 2 represents the CLI commands to run Airflow in standalone mode" width="800" height="101"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2. CLI commands to run Airflow in standalone mode from WSL 2.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I used the commands in Figure 2 to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Navigate to the Python application.&lt;/li&gt;
&lt;li&gt;Activate the Python virtual environment.&lt;/li&gt;
&lt;li&gt;Check which Airflow installation is being used.&lt;/li&gt;
&lt;li&gt;Run Airflow in standalone mode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once Airflow is running, navigate to &lt;code&gt;localhost:8080&lt;/code&gt;. Airflow uses this port by default. Access to the web interface is granted after submitting user credentials (username and password). &lt;/p&gt;

&lt;p&gt;Search for the DAG created for the pipeline by &lt;code&gt;dag_id&lt;/code&gt;. In this case, the &lt;code&gt;dag_id&lt;/code&gt; is &lt;code&gt;etl_dag&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Figure 3 showcases the successful creation and registration of &lt;code&gt;etl_dag&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmgffy5c5hunczegreb0s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmgffy5c5hunczegreb0s.png" alt="Figure 3 represents the Airflow web interface, showcasing the successful creation and registration of  raw `etl_dag` endraw ." width="800" height="304"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3. Airflow web interface showcasing the successful creation and registration of the &lt;code&gt;etl_dag&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Note: Make sure the DAG is activated by enabling the toggle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Monitor the data loading in the PostgreSQL database.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To verify that the data is being loaded, access the PostgreSQL database using pgAdmin.&lt;/p&gt;

&lt;p&gt;Figure 4 illustrates the insertion of the transformed data into the database using pgAdmin.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4cxxmyso2r8tu60gvj4p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4cxxmyso2r8tu60gvj4p.png" alt="Figure 4 represents a screenshot on pgAdmin illustrating the successful insertion of the data into the  raw `weather_data` endraw  table." width="800" height="421"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 4. pgAdmin interface illustrating the successful insertion of the data into the &lt;code&gt;weather_data&lt;/code&gt; table.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;The orchestration was successfully implemented and verified. The &lt;code&gt;DAG&lt;/code&gt; was created and registered, and its defined tasks were also validated. The transformed data was also successfully loaded into the PostgreSQL database.&lt;/p&gt;

&lt;p&gt;It is important to note that the current implementation is not hosted. Therefore, Airflow must be manually started whenever the pipeline needs to be executed. To ensure continuous execution of the pipeline, it can be hosted on cloud infrastructure. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://open-meteo.com/" rel="noopener noreferrer"&gt;Open-Meteo Weather API.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/k1ssa1/Apache-Airflow-ETL-Pipeline-With-Open-Meteo-API-" rel="noopener noreferrer"&gt;GitHub repository of the project.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://airflow.apache.org/docs/" rel="noopener noreferrer"&gt;Apache Airflow documentation.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/windows/wsl/about" rel="noopener noreferrer"&gt;WSL documentation - Microsoft official website.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trademark Notice&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python and the Python logos are trademarks of the Python Software Foundation (PSF).&lt;/li&gt;
&lt;li&gt;Postgres, PostgreSQL and the Slonik Logo are trademarks or registered trademarks of the PostgreSQL Community Association of Canada, and used with their permission.&lt;/li&gt;
&lt;li&gt;Apache Airflow, Airflow, and the Airflow logo are trademarks of the Apache Software Foundation.&lt;/li&gt;
&lt;li&gt;Ubuntu is a registered trademark of Canonical Ltd.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>airflow</category>
      <category>linux</category>
      <category>dataengineering</category>
      <category>automation</category>
    </item>
    <item>
      <title>Data Quality Management: Data Validation And Data Cleansing With Pandas and Pandera</title>
      <dc:creator>kitchen_code</dc:creator>
      <pubDate>Sat, 01 Aug 2026 21:36:14 +0000</pubDate>
      <link>https://dev.to/kitchen_code/data-quality-management-data-validation-and-data-cleansing-with-pandas-and-pandera-3elp</link>
      <guid>https://dev.to/kitchen_code/data-quality-management-data-validation-and-data-cleansing-with-pandas-and-pandera-3elp</guid>
      <description>&lt;p&gt;Data quality management (DQM) is a set of practices aimed at improving and maintaining the quality of an organisation's data. Effective DQM practices help ensure data accuracy, completeness, consistency, timeliness, uniqueness, and validity.&lt;/p&gt;

&lt;p&gt;In this article, I explore how to leverage Python libraries to carry out two fundamental DQM procedures: data validation and data cleansing.&lt;/p&gt;

&lt;p&gt;The dataset chosen for this exercise is the Kaggle dataset: &lt;a href="https://www.kaggle.com/datasets/ahmedmohamed2003/cafe-sales-dirty-data-for-cleaning-training" rel="noopener noreferrer"&gt;Cafe Sales - Dirty Data for Cleaning Training&lt;/a&gt; by Ahmed Mohamed, under the &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noopener noreferrer"&gt;CC BY-SA 4.0 license&lt;/a&gt;. It is a CSV file that comprises 10,000 cafe sales transactions across eight columns. Data quality issues are deliberately introduced, making it well suited for practicing DQM procedures.&lt;/p&gt;

&lt;p&gt;To implement these procedures, &lt;a href="https://pandas.pydata.org/docs/" rel="noopener noreferrer"&gt;Pandas&lt;/a&gt; is utilized for data cleansing, while &lt;a href="https://pandera.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;Pandera&lt;/a&gt; is used for data validation. &lt;/p&gt;

&lt;p&gt;Data validation requires establishing and enforcing business rules and checks, &lt;a href="https://www.ibm.com/think/topics/data-validation" rel="noopener noreferrer"&gt;according to IBM's data validation page&lt;/a&gt;. Each organization defines its own rules and checks. Nevertheless, we'll focus on the most common checks: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data type checks&lt;/li&gt;
&lt;li&gt;Uniqueness checks&lt;/li&gt;
&lt;li&gt;Format checks&lt;/li&gt;
&lt;li&gt;Code checks&lt;/li&gt;
&lt;li&gt;Consistency checks&lt;/li&gt;
&lt;li&gt;Range checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First, I created a data directory inside the Python application to store and access the CSV file. Next, I separated the extraction stage from the transformation stage by creating a dedicated module for each. &lt;a href="https://github.com/k1ssa1/Data-Quality-Management-with-Pandas-and-Pandera" rel="noopener noreferrer"&gt;Visit the GitHub repository to see the project structure&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The execution is applied in the main.py file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;workflow.extract&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;extract_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;workflow.transform&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform_data&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/dirty_cafe_sales.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;transform_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Extract the CSV file: extract.py
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  DQM procedures - data validation and data cleansing: transform.py
&lt;/h2&gt;

&lt;p&gt;I begin by creating a &lt;code&gt;transform_data(df)&lt;/code&gt; function that accepts the DataFrame returned by &lt;code&gt;extract_data&lt;/code&gt; in &lt;code&gt;extract.py&lt;/code&gt; as a parameter. &lt;/p&gt;

&lt;h3&gt;
  
  
  Rename columns
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Transaction ID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;transaction_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Item&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Quantity&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Price Per Unit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_per_unit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Total Spent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total_spent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Payment Method&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;payment_method&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Location&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Transaction Date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;transaction_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;inplace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Drop duplicates
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop_duplicates&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This removes the rows that are 100% identical across all columns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extract dirty data
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;data_extraction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;dirty_values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNKNOWN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;dirty_rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;rows_to_drop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterrows&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dirty_values&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;dirty_rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;rows_to_drop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="n"&gt;dirty_dataframe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dirty_rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;clean_dataframe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;rows_to_drop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;clean_dataframe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clean_dataframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;clean_dataframe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dirty_dataframe&lt;/span&gt;

    &lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;data_extraction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The objective of &lt;code&gt;data_extraction()&lt;/code&gt; is to separate clean rows from those containing data quality issues. &lt;/p&gt;

&lt;p&gt;The function first defines &lt;code&gt;dirty_values&lt;/code&gt;, which is a list of predefined invalid values (&lt;code&gt;UNKNOWN&lt;/code&gt;, &lt;code&gt;ERROR&lt;/code&gt;). It then iterates through each row of the DataFrame examining each cell. &lt;/p&gt;

&lt;p&gt;If a cell value matches the predefined ones in &lt;code&gt;dirty_values&lt;/code&gt; or is identified as a missing value (&lt;code&gt;NaN&lt;/code&gt;), it is therefore classified as a "dirty row". &lt;/p&gt;

&lt;p&gt;The dirty row is appended to the &lt;code&gt;dirty_rows&lt;/code&gt; list, while its index in the original DataFrame &lt;code&gt;df&lt;/code&gt; is stored in the &lt;code&gt;rows_to_drop&lt;/code&gt; list.&lt;/p&gt;

&lt;p&gt;Once all rows are processed, &lt;code&gt;dirty_rows&lt;/code&gt; is then converted to the DataFrame &lt;code&gt;dirty_dataframe&lt;/code&gt;. Figure 1 presents this DataFrame with all its records.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fstr5x4jrb4ktmm1vccv2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fstr5x4jrb4ktmm1vccv2.png" alt="Figure 1 represents a visualization of  raw `dirty_dataframe` endraw  with all its records" width="510" height="312"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1. Visualization of &lt;code&gt;dirty_dataframe&lt;/code&gt; and all its records containing data quality issues. This DataFrame stores 6911 rows extracted from the original DataFrame.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These dirty rows whose indexes are stored in &lt;code&gt;rows_to_drop&lt;/code&gt; are removed from &lt;code&gt;df&lt;/code&gt;, resulting in a new Dataframe: &lt;code&gt;clean_dataframe&lt;/code&gt;. Finally, the index of &lt;code&gt;clean_dataframe&lt;/code&gt; is reset to maintain sequential row numbering. Figure 2 presents the &lt;code&gt;clean_dataframe&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9uebns0hd273cl94tv2d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9uebns0hd273cl94tv2d.png" alt="Figure 2 represents a visualization of  raw `clean_dataframe` endraw  and all its records containing no data quality issues." width="523" height="316"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2. Visualization of &lt;code&gt;clean_dataframe&lt;/code&gt; and all its records containing no data quality issues. This DataFrame stores 3089 rows remaining from the original DataFrame.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the next steps, we will focus only on &lt;code&gt;clean_dataframe&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Data type conversion
&lt;/h3&gt;

&lt;p&gt;The relevant columns are converted to their appropriate data types. This ensures that numeric values are represented as integers or floats, while dates are stored using Pandas' &lt;code&gt;datetime&lt;/code&gt; type.&lt;/p&gt;

&lt;p&gt;The data type conversion is implemented as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price_per_unit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price_per_unit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_spent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_spent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transaction_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transaction_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coerce&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Data Type Check
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;To begin with, however, what is a data type check?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A data type check identifies values that violate the specified data type or do not conform to its expected length, precision, or scale. Therefore, after performing the data type conversion, Pandera is used to validate that the columns have the expected data types.&lt;/p&gt;

&lt;p&gt;Pandera provides two approaches to data validation: DataFrame Model and DataFrame Schema.&lt;/p&gt;

&lt;p&gt;For this exercise, I chose to use DataFrame Schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;data_type_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrameSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transaction_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price_per_unit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_spent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payment_method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transaction_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;data_type_check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After converting each relevant column to its appropriate data type, the &lt;code&gt;data_type_check&lt;/code&gt; schema is used to validate that the DataFrame complies with its rules.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Data validation can be performed either before or after data transformation, depending on the ETL pipeline design. In this case, the schema is applied after the type conversion to verify that the transformation was successful. Alternatively, it can be used before the data type conversion to identify records whose data types do not conform to the expected rules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Uniqueness Check
&lt;/h3&gt;

&lt;p&gt;A uniqueness check is applied to columns whose values must be unique. &lt;/p&gt;

&lt;p&gt;In our case, the uniqueness check can logically be applied only to the &lt;code&gt;transaction_id&lt;/code&gt; column. The other columns can store duplicate values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;uniqueness_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrameSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transaction_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;uniqueness_check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Format Check
&lt;/h3&gt;

&lt;p&gt;A format check is applied to columns that require specific data formatting such as email addresses or phone numbers. &lt;/p&gt;

&lt;p&gt;In our case, we conduct a format check on the &lt;code&gt;transaction_id&lt;/code&gt; column to verify that all the values start with &lt;code&gt;TXN_&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;format_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrameSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transaction_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TXN_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;format_check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Code Check
&lt;/h3&gt;

&lt;p&gt;A code check determines whether a data value is valid by comparing it to a list of acceptable values. &lt;/p&gt;

&lt;p&gt;In our case, we want to validate the &lt;code&gt;payment_method&lt;/code&gt; column against a set of acceptable codes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;transaction_methods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Credit Card&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Digital Wallet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cheque&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bank Transfers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;code_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrameSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payment_method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction_methods&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;code_check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We create the &lt;code&gt;transaction_methods&lt;/code&gt; list, which contains a set of valid payment methods. Then, we define the DataFrameSchema &lt;code&gt;code_check&lt;/code&gt; that uses a validation check to verify that a value is in &lt;code&gt;transaction_methods&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consistency Check
&lt;/h3&gt;

&lt;p&gt;Consistency checks are performed to verify that the relationships between two or more columns satisfy business rules.&lt;/p&gt;

&lt;p&gt;In this exercise, we examine whether the &lt;code&gt;total_spent&lt;/code&gt; values are consistent with the result of multiplying &lt;code&gt;quantity&lt;/code&gt; by &lt;code&gt;price_per_unit&lt;/code&gt; for each row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;consistency_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrameSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_spent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price_per_unit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;consistency_check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This schema uses a lambda function to define a custom validation rule. &lt;/p&gt;

&lt;h3&gt;
  
  
  Range Check
&lt;/h3&gt;

&lt;p&gt;A range check is a data validation check that determines whether numerical data falls within a predefined range of minimum and maximum values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;range_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrameSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transaction_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2023-01-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2024-01-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price_per_unit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_spent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;range_check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We examine whether &lt;code&gt;quantity&lt;/code&gt;, &lt;code&gt;price_per_unit&lt;/code&gt;, and &lt;code&gt;total_spent&lt;/code&gt; contain only non-negative values. We also verify that the dataset only includes rows with dates between January 1st, 2023, and December 31st, 2023.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sort the DataFrame by transaction date in descending order
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cd_sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;transaction_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ascending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Figure 3 presents the result of this transformation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl0mlt7bi70i18rhi5022.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl0mlt7bi70i18rhi5022.png" alt="Figure 3 presents the  raw `cd_sorted` endraw  DataFrame after conducting a sort by  raw `transaction_date` endraw  in descending order" width="528" height="314"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3. Visualization of &lt;code&gt;cd_sorted&lt;/code&gt; after conducting a sort by &lt;code&gt;transaction_date&lt;/code&gt; in descending order. This DataFrame stores 3089 rows remaining from the original DataFrame.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;The results of these DQM procedures are as follows: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cd_sorted&lt;/code&gt;: a clean DataFrame containing no identified data quality issues. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dd&lt;/code&gt;: a dirty DataFrame containing the rows with data quality issues extracted from the original dataset. The objective was not to permanently discard these records. Instead, they are preserved in &lt;code&gt;dd&lt;/code&gt; as they may contain valuable information that can be used for further investigation. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two distinct DataFrames can then be loaded into the destination of choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.ibm.com/think/topics/data-quality-management#1703063718" rel="noopener noreferrer"&gt;Data quality management, defined - IBM official website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ibm.com/think/topics/data-validation" rel="noopener noreferrer"&gt;What is data validation? - IBM official website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kaggle.com/datasets/ahmedmohamed2003/cafe-sales-dirty-data-for-cleaning-training" rel="noopener noreferrer"&gt;Cafe Sales - Dirty Data for Cleaning Training - by Ahmed Mohamed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/k1ssa1/Data-Quality-Management-with-Pandas-and-Pandera" rel="noopener noreferrer"&gt;GitHub repositry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pandas.pydata.org/" rel="noopener noreferrer"&gt;Pandas official documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pandera.readthedocs.io/en/stable/index.html" rel="noopener noreferrer"&gt;Pandera official documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataengineering</category>
      <category>pandas</category>
      <category>pandera</category>
      <category>python</category>
    </item>
    <item>
      <title>Incremental Relational Normalization of Semi-Structured API Product Data: An ELT Pipeline Case Study</title>
      <dc:creator>kitchen_code</dc:creator>
      <pubDate>Mon, 27 Jul 2026 01:50:15 +0000</pubDate>
      <link>https://dev.to/kitchen_code/incremental-relational-normalization-of-semi-structured-api-product-data-an-elt-pipeline-case-1o0m</link>
      <guid>https://dev.to/kitchen_code/incremental-relational-normalization-of-semi-structured-api-product-data-an-elt-pipeline-case-1o0m</guid>
      <description>&lt;p&gt;Check out my &lt;a href="https://dev.to/kitchen_code/incremental-relational-normalization-of-semi-structured-api-product-data-an-elt-pipeline-case-study-54l1"&gt;second article&lt;/a&gt;. I would be really happy to have your feedback.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>postgres</category>
      <category>restapi</category>
      <category>sql</category>
    </item>
    <item>
      <title>Incremental Relational Normalization of Semi-Structured API Product Data: An ELT Pipeline Case Study</title>
      <dc:creator>kitchen_code</dc:creator>
      <pubDate>Sat, 25 Jul 2026 23:51:12 +0000</pubDate>
      <link>https://dev.to/kitchen_code/incremental-relational-normalization-of-semi-structured-api-product-data-an-elt-pipeline-case-study-54l1</link>
      <guid>https://dev.to/kitchen_code/incremental-relational-normalization-of-semi-structured-api-product-data-an-elt-pipeline-case-study-54l1</guid>
      <description>&lt;p&gt;In my previous case study, I built an ETL pipeline using CSV files as the data source. Since CSV is a structured format and the dataset was relatively clean, the transformation stage was more predictable, allowing me to focus on understanding the overall ETL workflow rather than exploring each stage in great technical depth.&lt;/p&gt;

&lt;p&gt;For this new case study, I wanted to explore a different type of challenge by building an ELT pipeline using semi-structured data. At the same time, I was exploring relational database design and normalization principles, and I realized that they would naturally fit into the transformation phase.&lt;/p&gt;

&lt;p&gt;This article presents the development of an ELT pipeline that combines these objectives: ingesting nested JSON data from a REST API using a Python application, loading the raw data into a PostgreSQL database, and incrementally transforming it into a normalized relational data model through SQL-based transformations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Methods
&lt;/h2&gt;

&lt;h3&gt;
  
  
  System architecture
&lt;/h3&gt;

&lt;p&gt;Figure 1 illustrates the high-level architecture of the ELT pipeline implemented in this case study.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7qzgkcgptri93ep8o75c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7qzgkcgptri93ep8o75c.png" alt="Figure 1. High-level architecture of the data ELT pipeline." width="800" height="391"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1. High-level architecture of the data ELT pipeline.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  REST API Endpoint
&lt;/h3&gt;

&lt;p&gt;The dataset was obtained from the public DummyJSON REST API, which is released under the MIT license. The API exposes multiple resources; however, but this exercise focuses on the &lt;code&gt;/products&lt;/code&gt; endpoint. &lt;/p&gt;

&lt;p&gt;To perform the extraction and loading stages of the pipeline, I developed a Python application within a virtual environment (&lt;code&gt;venv&lt;/code&gt;). Following the Separation of Concerns (SoC) principle, I implemented each stage in a separate module to improve modularity.&lt;/p&gt;
&lt;h4&gt;
  
  
  Extraction Stage
&lt;/h4&gt;

&lt;p&gt;First, I installed the &lt;code&gt;requests&lt;/code&gt; library to enable the application to send HTTP requests to the DummyJSON REST API and retrieve JSON data from the &lt;code&gt;/products&lt;/code&gt; endpoint. &lt;/p&gt;

&lt;p&gt;The response was parsed into a Python dictionary, from which the &lt;code&gt;products&lt;/code&gt; array was extracted. This allowed the application to iterate over each product individually before the loading stage.&lt;/p&gt;
&lt;h4&gt;
  
  
  Loading Stage
&lt;/h4&gt;

&lt;p&gt;Using &lt;code&gt;psycopg3&lt;/code&gt;, I established a connection between the application and the PostgreSQL database &lt;code&gt;product_catalog&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Each product record was loaded as a raw &lt;code&gt;JSONB&lt;/code&gt; document into the &lt;code&gt;raw_products_data&lt;/code&gt; table, preserving the original structure before transformation. Figure 2 presents the data model of &lt;code&gt;raw_products_data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff173bp6xhc9us7not9dh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff173bp6xhc9us7not9dh.png" alt="Figure 2. Data model of the raw_products_data table used to store raw product records as  raw `JSONB` endraw  documents." width="149" height="94"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2. Data model of the &lt;code&gt;raw_products_data&lt;/code&gt; table used to store raw product records as &lt;code&gt;JSONB&lt;/code&gt; documents.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Figure 3 demonstrates that &lt;code&gt;raw_products_data&lt;/code&gt; was successfully populated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5r0pellbl6q2d5o039dv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5r0pellbl6q2d5o039dv.png" alt="Raw product records stored as  raw `JSONB` endraw  documents in  raw `raw_products_data` endraw  before any SQL-based transformations and normalization." width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3. Raw product records stored as &lt;code&gt;JSONB&lt;/code&gt; documents in the &lt;code&gt;raw_products_data&lt;/code&gt; table before any SQL-based transformations or normalization.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The next step is to transform the raw data into a normalized relational data model.&lt;/p&gt;
&lt;h4&gt;
  
  
  Transformation Stage
&lt;/h4&gt;

&lt;p&gt;This stage represents the main focus of this study. The transformation is centered on the application of normalization principles to convert the raw &lt;code&gt;JSONB&lt;/code&gt; documents into a structured relational data model.&lt;/p&gt;

&lt;p&gt;After establishing the initial relational model, higher normal forms are evaluated to determine whether further decomposition is required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First Normal Form (1NF):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since the &lt;code&gt;JSONB&lt;/code&gt; documents contain nested arrays and objects, their attributes are not represented in atomic values. This violates the requirements of First Normal Form, which states that each attribute must contain an atomic value. &lt;/p&gt;

&lt;p&gt;I created the &lt;code&gt;products_stage&lt;/code&gt; table to store the atomic attributes associated with each product. Top-level scalar attributes were mapped directly to columns, while nested objects were flattened into individual columns. The &lt;code&gt;tags&lt;/code&gt;, &lt;code&gt;images&lt;/code&gt;, and &lt;code&gt;reviews&lt;/code&gt; attributes were intentionally excluded because they represent multi-valued attributes or relationships requiring separate relational tables.&lt;/p&gt;

&lt;p&gt;The following SQL statement creates the &lt;code&gt;products_stage&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;products_stage&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;discount_percentage&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;rating&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;brand&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;dimension_width&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;dimension_height&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;dimension_depth&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;warranty_information&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;shipping_information&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;availability_status&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;return_policy&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;minimum_order_quantity&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;barcode&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;qr_code&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;thumbnail&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compared with &lt;code&gt;raw_products_data&lt;/code&gt; shown in Figure 2, Figure 4 illustrates the data model of the products_stage table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9q52s9w5dsdt1f4uhrw1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9q52s9w5dsdt1f4uhrw1.png" alt="Figure 4. Data model of the products_stage containing only atomic values." width="200" height="534"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 4. Data model of the &lt;code&gt;products_stage&lt;/code&gt; containing only atomic values.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After creating the table, the following &lt;code&gt;INSERT&lt;/code&gt; statement was executed to populate it with data extracted from the &lt;code&gt;JSONB&lt;/code&gt; documents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;products_stage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;discount_percentage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimension_width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;dimension_height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimension_depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;warranty_information&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shipping_information&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;availability_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;minimum_order_quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;barcode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qr_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thumbnail&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'description'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'price'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'discountPercentage'&lt;/span&gt;
  &lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'rating'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'stock'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'brand'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sku'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'weight'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'dimensions'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'width'&lt;/span&gt;
  &lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'dimensions'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'height'&lt;/span&gt;
  &lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'dimensions'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'depth'&lt;/span&gt;
  &lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'warrantyInformation'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'shippingInformation'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'availabilityStatus'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'returnPolicy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'minimumOrderQuantity'&lt;/span&gt;
  &lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'createdAt'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'updatedAt'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'barcode'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'meta'&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'qrCode'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="n"&gt;raw_data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'thumbnail'&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; 
  &lt;span class="n"&gt;raw_products_data&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;INSERT&lt;/code&gt; statement uses &lt;a href="https://www.postgresql.org/docs/current/functions-json.html" rel="noopener noreferrer"&gt;PostgreSQL's JSONB operators&lt;/a&gt; (&lt;code&gt;-&amp;gt;&lt;/code&gt; and &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt;) to navigate the JSON structure, extract values, and cast them to the appropriate SQL data types.&lt;/p&gt;

&lt;p&gt;Figure 5 presents a sample of the populated &lt;code&gt;products_stage&lt;/code&gt; table in PostgreSQL, showing a subset of its columns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe5vbliysooereluf3a7o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe5vbliysooereluf3a7o.png" alt="Figure 5 represents a Sample output of the populated  raw `products_stage` endraw  table in PostgreSQL." width="800" height="328"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 5. Sample output of the populated &lt;code&gt;products_stage&lt;/code&gt; table in PostgreSQL.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Although the &lt;code&gt;products_stage&lt;/code&gt; table satisfies the requirements of the First Normal Form (1NF), the complete relational model is not yet finalized. The excluded attributes must still be extracted into dedicated tables to ensure that multi-valued attributes are properly represented withtin the relational model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create the &lt;code&gt;product_images&lt;/code&gt; table&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To normalize the multi-valued &lt;code&gt;images&lt;/code&gt; attribute, I created the &lt;code&gt;product_images&lt;/code&gt; table. &lt;/p&gt;

&lt;p&gt;Since a single product can have multiple images, the relationship between &lt;code&gt;products_stage&lt;/code&gt; and &lt;code&gt;product_images&lt;/code&gt; is one-to-many (1:N). Each image is therefore stored as a  separate row and associated with its corresponding product through a foreign key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;product_images&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;image_id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;image_url&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_product_images&lt;/span&gt;
        &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;products_stage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Populate the &lt;code&gt;product_images&lt;/code&gt; table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;product_images&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;jsonb_array_elements_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'images'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;raw_products_data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;jsonb_array_elements_text()&lt;/code&gt; function expands each element of the &lt;code&gt;images&lt;/code&gt; array into a separate row, allowing each image URL to be stored as an atomic value. &lt;/p&gt;

&lt;p&gt;The updated relational model is shown in Figure 6.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyotdopct1ehk9uxugjl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyotdopct1ehk9uxugjl.png" alt="Figure 6 illustrates the updated relational model showing the one-to-many (1:N) relationship between  raw `products_stage` endraw  and the  raw `product_images` endraw ." width="385" height="534"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 6. Updated relational model showing the one-to-many (1:N) relationship between &lt;code&gt;products_stage&lt;/code&gt; and &lt;code&gt;product_images&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create the &lt;code&gt;product_reviews&lt;/code&gt; table&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The relationship between &lt;code&gt;products_stage&lt;/code&gt; and the new &lt;code&gt;product_reviews&lt;/code&gt; table is also a one-to-many relationship since one product can have multiple reviews.&lt;/p&gt;

&lt;p&gt;The following SQL statement creates &lt;code&gt;product_reviews&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;product_reviews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;review_id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;review_rating&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;review_comment&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;review_date&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reviewer_name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;reviewer_email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_product_reviews&lt;/span&gt;
        &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;products_stage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Populate &lt;code&gt;product_reviews&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;product_reviews&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;review_rating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;review_comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;review_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reviewer_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reviewer_email&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'rating'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'comment'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'date'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'reviewerName'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'reviewerEmail'&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;raw_products_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;jsonb_array_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'reviews'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;review&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;jsonb_array_elements()&lt;/code&gt; function was used to iterate over the &lt;code&gt;reviews&lt;/code&gt; array of JSON objects in each row of &lt;code&gt;raw_products_data&lt;/code&gt;. Each review object was expanded into a separate row, allowing its individual attributes to be extracted and stored as atomic values.&lt;/p&gt;

&lt;p&gt;The updated relational schema is shown in Figure 7.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fta02rpbokwjym073fgfj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fta02rpbokwjym073fgfj.png" alt="Figure 7 illustrates the updated model showing the one-to-many (1:N) relationship between  raw `products_stage` endraw  and  raw `product_reviews` endraw ." width="407" height="534"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 7. Updated relational model showing the one-to-many (1:N) relationship between the &lt;code&gt;products_stage&lt;/code&gt; table and the &lt;code&gt;product_reviews&lt;/code&gt; table.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create the &lt;code&gt;tags&lt;/code&gt; table&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unlike the &lt;code&gt;images&lt;/code&gt; and &lt;code&gt;reviews&lt;/code&gt; attributes, the relationship between products and &lt;code&gt;tags&lt;/code&gt; is many-to-many (M:N). A product can have multiple tags, and a tag can be associated with many products.&lt;/p&gt;

&lt;p&gt;To model this relationship, I created a &lt;code&gt;tags&lt;/code&gt; table that stores each unique tag only once. The following SQL statement creates the &lt;code&gt;tags&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tag_id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tag_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Populate the &lt;code&gt;tags&lt;/code&gt; table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt;
    &lt;span class="n"&gt;jsonb_array_elements_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'tags'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;raw_products_data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I created the junction table &lt;code&gt;product_tags&lt;/code&gt; to represent the many-to-many relationship between products with their corresponding tags through foreign keys. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create the &lt;code&gt;product_tags&lt;/code&gt; table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;product_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tag_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_product_tags_product&lt;/span&gt;
        &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;products_stage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_product_tags_tag&lt;/span&gt;
        &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Populate the &lt;code&gt;product_tags&lt;/code&gt; table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;product_tags&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;raw_products_data&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;jsonb_array_elements_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'tags'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;tag_name&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tag_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;CROSS JOIN&lt;/code&gt; with &lt;code&gt;jsonb_array_elements_text()&lt;/code&gt; expands the &lt;code&gt;tags&lt;/code&gt; array of each product into individual rows, producing one row for each "product-tag" combination. These tag values are then matched with the corresponding records in the &lt;code&gt;tags&lt;/code&gt; table using an &lt;code&gt;INNER JOIN&lt;/code&gt;, allowing the &lt;code&gt;tag_id&lt;/code&gt; values to be retrieved. Finally, the &lt;code&gt;DISTINCT&lt;/code&gt; keyword ensures that each &lt;code&gt;(product_id, tag_id)&lt;/code&gt; pair is inserted only once into &lt;code&gt;product_tags&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The updated relational schema is shown in Figure 8.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm8p9h8yyiw9mb1t0npc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm8p9h8yyiw9mb1t0npc.png" alt="Figure 8 illustrates the updated relational model showing the many-to-many (M:N) relationship between  raw `products_stage` endraw  and  raw `tags` endraw , implemented through the  raw `product_tags` endraw  junction table." width="375" height="678"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 8. Updated relational model showing the many-to-many (M:N) relationship between &lt;code&gt;products_stage&lt;/code&gt; and &lt;code&gt;tags&lt;/code&gt;, implemented through the &lt;code&gt;product_tags&lt;/code&gt; junction table.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second Normal Form (2NF):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a relational model to satisfy the requirements of the Second Normal Form (2NF), it must first comply with the rules of the First Normal Form (1NF). &lt;/p&gt;

&lt;p&gt;In the previous section, all tables were normalized to satisfy this prerequisite.&lt;/p&gt;

&lt;p&gt;The next step is to examine whether each table in the 1NF relational model satisfies the second requirement of 2NF. This involves determining whether a non-key attribute within a table depends on only a subset of a composite primary key than the entire key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;products_stage&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;products_stage&lt;/code&gt; complies with 2NF. Since the primary key of the table is composed of a single attribute &lt;code&gt;product_id&lt;/code&gt; (see Figure 4), partial dependencies cannot occur. Furthermore, all non-key attributes describe the product associated with &lt;code&gt;product_id&lt;/code&gt; and are therefore fully functionally dependent on the primary key. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;product_images&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;product_images&lt;/code&gt; fulfills the conditions of 2NF. Since the primary key consists of a single attribute &lt;code&gt;image_id&lt;/code&gt; (see &lt;code&gt;product_images&lt;/code&gt; in Figure 6), partial dependencies cannot occur. In addition, the non-key attributes, &lt;code&gt;product_id&lt;/code&gt; and &lt;code&gt;image_url&lt;/code&gt;, describe the image identified by &lt;code&gt;image_id&lt;/code&gt; and are therefore fully functionally dependent on the primary key. &lt;/p&gt;

&lt;p&gt;A common misconception is to assume that &lt;code&gt;image_url&lt;/code&gt; depends on &lt;code&gt;product_id&lt;/code&gt; because a product can have multiple images. However, that is not how the table is designed.&lt;/p&gt;

&lt;p&gt;Each row represents a single image entity identified by &lt;code&gt;image_id&lt;/code&gt;, while &lt;code&gt;product_id&lt;/code&gt; simply establishes the relationship between the image and its corresponding product. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;product_reviews&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;product_reviews&lt;/code&gt; satisfies the criteria for 2NF. Since the primary key consists of a single attribute &lt;code&gt;review_id&lt;/code&gt; (see &lt;code&gt;product_reviews&lt;/code&gt; in Figure 7), partial dependencies cannot occur. Furthermore, all non-key attributes are fully functionally dependent on the primary key.&lt;/p&gt;

&lt;p&gt;Each row represents a single review entity identified by &lt;code&gt;review_id&lt;/code&gt;, while &lt;code&gt;product_id&lt;/code&gt; simply establishes the relationship between the review and its corresponding product. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;tags&lt;/code&gt; and &lt;code&gt;product_tags&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tags&lt;/code&gt; satisfies 2NF. Since the primary key consists of a single attribute &lt;code&gt;tag_id&lt;/code&gt; (see &lt;code&gt;tags&lt;/code&gt; in Figure 8), partial dependencies cannot occur. Furthermore, the non-key attribute &lt;code&gt;tag_name&lt;/code&gt; is fully functionally dependent on the primary key. &lt;/p&gt;

&lt;p&gt;Unlike the previous tables, the &lt;code&gt;product_tags&lt;/code&gt; table uses a composite primary key composed of &lt;code&gt;product_id&lt;/code&gt; and &lt;code&gt;tag_id&lt;/code&gt; (see &lt;code&gt;product_tags&lt;/code&gt; in Figure 8).&lt;/p&gt;

&lt;p&gt;Each row represents the association between a product and a tag, uniquely identified by the combination of &lt;code&gt;product_id&lt;/code&gt; and &lt;code&gt;tag_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, the table contains no non-key attributes so no partial dependencies can exist. As a result, &lt;code&gt;product_tags&lt;/code&gt; also satisfies the requirements of 2NF.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third Normal Form (3NF):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a relational model to comply with the conditions of the Third Normal Form (3NF), it must first satisfy the rules of 2NF. &lt;/p&gt;

&lt;p&gt;In the previous section, all tables were normalized to satisfy this prerequisite.&lt;/p&gt;

&lt;p&gt;The next step is to determine whether each table in the 2NF relational model satisfies 3NF. &lt;/p&gt;

&lt;p&gt;During the analysis of each table, potential functional dependency candidates are identified and evaluated to determine whether they could introduce transitive dependencies.  In other words, the objective is to identify if any non-key attribute within a table is transitively dependent on another non-key attribute in the same table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;products_stage&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;During the analysis, it was observed that products with &lt;code&gt;stock&lt;/code&gt; value lower than 8 were consistently assigned the value "Low stock" in their &lt;code&gt;availability_status&lt;/code&gt; attribute. Conversely, products with a &lt;code&gt;stock&lt;/code&gt; value of 8 or greater were consistently assigned "In stock". This suggests a possible dependency between &lt;code&gt;stock&lt;/code&gt; and &lt;code&gt;availability_status&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;However, this observation alone is insufficient to establish the existence of a functional dependency. As a result, the 3NF status of this table could not be determined.&lt;/p&gt;

&lt;p&gt;Although removing the &lt;code&gt;availability_status&lt;/code&gt; attribute would eliminate the potential dependency, it would alter the source data without sufficient proof that the attribute is reundundant.&lt;/p&gt;

&lt;p&gt;Since the objective in this case study is to normalize the source data rather than modify its underlying structure, the attribute &lt;code&gt;availability_status&lt;/code&gt; was retained in the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;product_images&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since a product may have multiple images, &lt;code&gt;product_id&lt;/code&gt; does not functionally determine &lt;code&gt;image_url&lt;/code&gt;; therefore, no transitive dependency exists between non-key attributes. Every non-key attribute depends directly on the primary key &lt;code&gt;image_id&lt;/code&gt;, thus satisfying 3NF. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;product_reviews&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;During the examination, it was determined that each &lt;code&gt;reviewer_email&lt;/code&gt; value was consistently associated with one &lt;code&gt;reviewer_name&lt;/code&gt; suggesting a possible functional dependency between the two non-key attributes. However, this observation alone is insufficient to determine the existence of a functional dependency. As a result, the 3NF status of this table could not be determined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyze &lt;code&gt;tags&lt;/code&gt; and &lt;code&gt;product_tags&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;tags&lt;/code&gt; table satisfies 3NF. It contains only one non-key attribute &lt;code&gt;tag_name&lt;/code&gt;, which depends directly on the primary key &lt;code&gt;tag_id&lt;/code&gt;. Therefore, no transitive dependency can exist. The same reasoning applies to the &lt;code&gt;product_tags&lt;/code&gt; table. &lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;First Normal Form (1NF)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The raw product data was initially stored in &lt;code&gt;raw_products_data&lt;/code&gt; as &lt;code&gt;JSONB&lt;/code&gt; documents. &lt;/p&gt;

&lt;p&gt;During the 1NF transformation, the semi-structured data was converted into a set of relational tables: &lt;code&gt;products_stage&lt;/code&gt;, &lt;code&gt;product_images&lt;/code&gt;, &lt;code&gt;product_reviews&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt; and &lt;code&gt;product_tags&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Nested arrays and objects were eliminated and each attribute was stored as an atomic value while preserving the relationships between the entities through primary and foreign keys. &lt;/p&gt;

&lt;p&gt;The resulting relational model satisfied the First Normal Form (1NF). Figure 9 presents the complete relational model obtained after the 1NF transformation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcdxadcollabaodoqxcn4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcdxadcollabaodoqxcn4.png" alt="Figure 9. Final relational model satisfying the requirements of 1NF, derived from the raw JSONB product data." width="407" height="690"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 9. Final relational schema satisfying the requirements of the First Normal Form 1NF, derived from the raw JSONB product data.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second Normal Form (2NF)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The analysis showed that no partial dependencies exist within the relational model. Tables containing single-attribute primary keys (&lt;code&gt;products_stage&lt;/code&gt;, &lt;code&gt;product_images&lt;/code&gt;, &lt;code&gt;product_reviews&lt;/code&gt;, and &lt;code&gt;tags&lt;/code&gt;) cannot exhibit partial dependencies because every non-key attribute is fully functionally dependent on its primary key. &lt;/p&gt;

&lt;p&gt;Although the &lt;code&gt;product_tags&lt;/code&gt; table contains a composite primary key (&lt;code&gt;product_id&lt;/code&gt;, &lt;code&gt;tag_id&lt;/code&gt;), it stores no non-key attributes. Consequently, no partial dependencies can exist, and the table therefore does not violate the Second Normal Form (2NF) requirements.&lt;/p&gt;

&lt;p&gt;No modifications were required ater the 2NF evaluation. The relational data model remains identical to the one shown in Figure 9.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third Normal Form (3NF)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The 3NF analysis identified two categories of tables within the relational model. &lt;/p&gt;

&lt;p&gt;The first category consists of the &lt;code&gt;product_images&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;, and &lt;code&gt;product_tags&lt;/code&gt;, for which no transitive dependencies were identified. These tables comply with 3NF.&lt;/p&gt;

&lt;p&gt;The second category consists of &lt;code&gt;products_stage&lt;/code&gt; and &lt;code&gt;product_reviews&lt;/code&gt;, where potential functional dependencies were observed. However, the available data was insufficient to conclusively establish these dependencies. Therefore, their compliance with 3NF could not be conclusively established.&lt;/p&gt;

&lt;p&gt;As a result no modifications to the relational model were required following the 3NF analysis. The final relational model therefore remains identical to the model presented in Figure 9.&lt;/p&gt;

&lt;p&gt;The resulting relational model was normalized to 2NF. Although 3NF was established for some tables, the 3NF status of the complete relational model could not be determined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;This case study set out to explore an ELT approach for handling semi-structured data and transforming it into a relational data model. Beyond this objective, the study also strengthened both my practical and theoretical skills.&lt;/p&gt;

&lt;p&gt;The practical side involves performing HTTP requests with the &lt;code&gt;requests&lt;/code&gt; library to consume a REST API in Python. It also involves working extensively with SQL in PostgreSQL by using &lt;code&gt;JSONB&lt;/code&gt; operators and set-returning functions. &lt;/p&gt;

&lt;p&gt;On the theorical side, the exercise provided hands-on application of normalization principles requiring the evaluation of the data models across the normal forms. In addition, it deepened my understanding of data modeling, particularly how relationships (1:N and M:N) are represented through foreign keys and junction tables as the model evolves. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/k1ssa1/api-to-postgresql-elt-pipeline" rel="noopener noreferrer"&gt;GiHub repository of the Python application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dummyjson.com/" rel="noopener noreferrer"&gt;Product semi-structured dataset: DummyJSON official website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dbeaver.io/" rel="noopener noreferrer"&gt;Data visualisation tool for the ER diagrams: DBeaver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.diagrams.net/" rel="noopener noreferrer"&gt;Pipeline architecture design tool: draw.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>elt</category>
      <category>normalization</category>
      <category>sql</category>
      <category>restapi</category>
    </item>
    <item>
      <title>Building my first ETL Pipeline: A Healthcare Management System case study</title>
      <dc:creator>kitchen_code</dc:creator>
      <pubDate>Mon, 20 Jul 2026 00:26:02 +0000</pubDate>
      <link>https://dev.to/kitchen_code/building-my-first-etl-pipeline-a-healthcare-management-system-case-study-2ba3</link>
      <guid>https://dev.to/kitchen_code/building-my-first-etl-pipeline-a-healthcare-management-system-case-study-2ba3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Data engineering has long been a field of interest to me. Drawing upon my software engineering background, particularly in web development, I decided to undertake a structured learning path to master its core concepts. This article presents my first hands-on exercise in data engineering: the development of an ETL pipeline for a healthcare management system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For this introductory case study, I chose to build an ETL pipeline because it provides a straightforward way to understand the three fundamental stages of the process: extraction, transformation and loading. By contrast, ELT pipelines are typically used within data warehouse or data lakehouse environments, adding another layer of concepts beyond the scope of this introductory exercise. &lt;/p&gt;

&lt;p&gt;To support this case study, I selected the publicly available Healthcare Management System dataset published on Kaggle by Anouska Abhisikta. The dataset consists of five CSV files representing a healthcare management system: Appointment, Billing, Doctor, Medical Procedure and Patient. Its relational structure is particularly suitable for a introductory ETL pipeline practical demonstration. In addition, I deliberately selected a CSV-based dataset to avoid the additional complexity of formats such as JSON and Parquet files, or other data sources that would introduce distractions from the core ETL concepts explored in this exercise.&lt;/p&gt;

&lt;p&gt;The objective is to build an ETL pipeline that extracts the data from these CSV files, applies data transformations, and loads the transformed data into a local PostgreSQL database. &lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;METHOD:&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The ETL pipeline was developed as a batch processing application using Python. A dedicated virtual environment (venv) was created in order to isolate project dependencies, with Pandas serving as the library for data manipulation. The source CSV files were stored in a data directory within the project and served as the input source throughout the pipeline. &lt;/p&gt;

&lt;p&gt;The Python application follows the principle of separation of concerns, with each stage of the ETL pipeline being implemented in a separate module to improve readability, while main.py coordinates the execution of the pipeline (figure 1).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1wq3m9aeumsdrgk4lbsz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1wq3m9aeumsdrgk4lbsz.png" alt="Figure 1 - Python Batch ETL project structure" width="336" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STAGE 1: EXTRACTION&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;During the extraction stage, each CSV file is independently read in its own dedicated module and the extraction function returns a Pandas DataFrame, marking the first stage of the ETL pipeline (Figure 2).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrswsc5eotvvzhyhplmq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrswsc5eotvvzhyhplmq.png" alt="Figure 2 - Example of the implementation of the extraction phase on the Appointment CSV dataset" width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STAGE 2: TRANSFORMATION&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The transformation stage demonstrates basic data processing techniques using the Python library Pandas. Given that this is a beginner case study, this data transformation is intentionally simple and is intended to understand the Transform phase rather than performing complex data manipulation strategies. Depending on the use case, different transformation rules may be applied to the same dataset. &lt;/p&gt;

&lt;p&gt;Figure 3 illustrates an example of the data transformation performed on the Appointment dataset. This includes duplicate removal (drop_duplicates()), data type conversion, column renaming (rename()), and the creation of surrogate primary keys. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8i0tns54eao04uzice0k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8i0tns54eao04uzice0k.png" alt="Figure 3 - Example of the implementation of the transformation phase on the Appointment DataFrame" width="800" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STAGE 3: LOAD&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To load the transformed data, the Psycopg2 library was used to establish a connection between my Python application and a local PostgreSQL database. To centralize the database connection logic, a database.py file was created as shown in figure 4.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tnp4kn0zvtewrvz1iu6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tnp4kn0zvtewrvz1iu6.png" alt="Figure 4 - Establishing the connection to the PostgreSQL database using the library Psyocpg2" width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As an additional security layer, environment variables were used to avoid hardcoding credentials in the source code. Accordingly, the python-dotenv library was used to load these environment variables into the application at runtime.&lt;/p&gt;

&lt;p&gt;Once the database connection is established, dedicated loading functions were implemented for each transformed dataset. These functions follow the same workflow. First, a database cursor is created to execute SQL statements. Then, the corresponding table is created if it does not already exist, and the transaction is committed before the insertion phase. This ensures that the table is successfully created and permanently saved, even if an error occurs during data insertion. &lt;/p&gt;

&lt;p&gt;The transformed dataset is then iterated row by row so that each record is inserted into the corresponding table in the local PostgreSQL database. The transaction is committed again to permanently save the inserted data and the cursor is closed to release the associated resources. Figure 5 presents an example of the loading phase implementation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wec7gjn5vqd23w2vcok.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wec7gjn5vqd23w2vcok.png" alt="Figure 5 - Example of the implementation of the loading phase of the transformed Appointment data" width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OPTIONAL:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As an introduction to containerization concepts, the Python application was packaged using Docker. A Dockerfile was created to define the instructions required to build the Docker image as illustrated in figure 6.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo9ylcepflh1sjhy8tuly.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo9ylcepflh1sjhy8tuly.png" alt="Figure 6 - Defining the instructions in the Dockerfile for the image creation" width="714" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Only the Python application was containerized, while the PostgreSQL database is kept running locally outside the image. Figure 7 presents the global architecture of this exercise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fodqdrryvrujt8993oa93.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fodqdrryvrujt8993oa93.png" alt="Figure 7 - the global architecture of the exercise" width="800" height="671"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;RESULT&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The main.py file plays a central role in coordinating the execution of the ETL pipeline. It acts as the entry point of the application by orchestrating the sequential execution of the different stages for each dataset entity.&lt;/p&gt;

&lt;p&gt;The complete implementation of this file is shown in the code block below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;extract.patient&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;extract_patient_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transform.patient&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform_patient_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;load.patient&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_patient_data&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;extract.doctor&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;extract_doctor_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transform.doctor&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform_doctor_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;load.doctor&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_doctor_data&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;extract.appointment&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;extract_appointment_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transform.appointment&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform_appointment_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;load.appointment&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_appointment_data&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;extract.billing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;extract_billing_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transform.billing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform_billing_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;load.billing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_billing_data&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;extract.medical_proecdure&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;extract_med_procedure_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transform.medical_procedure&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform_med_procedure_data&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;load.medical_procedure&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_med_procedure_data&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_connection&lt;/span&gt;

&lt;span class="n"&gt;patient_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_patient_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;patient_table_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_patient_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;patient_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;doctor_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_doctor_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;doctor_table_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_doctor_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doctor_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;appointment_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_appointment_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;appointment_table_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_appointment_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;appointment_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;billing_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_billing_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;billing_data_table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_billing_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;billing_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;procedure_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_med_procedure_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;procedure_data_table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transform_med_procedure_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;procedure_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Connected to database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;load_patient_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;patient_table_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;load_doctor_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doctor_table_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;load_appointment_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;appointment_table_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;load_billing_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;billing_data_table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;load_med_procedure_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;procedure_data_table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to connect to database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the execution of the main.py file, the pipeline successfully extracted data from the CSV files, applied the predefined transformations and loaded the transformed data into the PostgreSQL database. All relational tables were created as shown in Figure 8 and 9.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5nw5lg60g8vb9s0x8ijx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5nw5lg60g8vb9s0x8ijx.png" alt="Figure 8 - Successful creation the tables on PostgreSQL" width="343" height="680"&gt;&lt;/a&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsak1l4wolcc2b53kxn38.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsak1l4wolcc2b53kxn38.png" alt="Figure 9 - Successful insertion of the data into the tables" width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;In conclusion, this study provided a practial experience with the fundamental concepts involved in developing a batch ETL pipeline. It enbales you to gain experience in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structuring Python projects with separation of concerns (modular packages per ETL stage).&lt;/li&gt;
&lt;li&gt;Environment isolation with venv and dependency management with Pip.&lt;/li&gt;
&lt;li&gt;Hands-on pandas transformations.&lt;/li&gt;
&lt;li&gt;Connection Python applications to a local PostgreSQL database, with psycopg2 and safe credential handling using environment variables.&lt;/li&gt;
&lt;li&gt;Basic Docker fundamentals (Dockerfile, building image, running containers).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As of now, the pipeline does not include an orchestration tool for scheduling or automating executions, and the PostgreSQL database is not containerized so the setup isn’t fully portable. These design choices are made to keep this exercise focused on understanding core ETL concepts while adding a basic application of Docker features. Future work will focus on different data sources such as Json, Parquet or perhaps a REST API and eventually explore ELT patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Resources&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.kaggle.com/datasets/anouskaabhisikta/healthcare-management-system" rel="noopener noreferrer"&gt;Kaggle Healthcare Management System dataset&lt;/a&gt; (under the Apache 2.0 license)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/k1ssa1/healthcare-management-system-etl-pipeline-miniproject" rel="noopener noreferrer"&gt;Github repository of this exercise&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dataengineering</category>
      <category>etl</category>
      <category>docker</category>
      <category>python</category>
    </item>
  </channel>
</rss>
