<?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>Forem: Tun</title>
    <description>The latest articles on Forem by Tun (@stereosky).</description>
    <link>https://forem.com/stereosky</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F12945%2F6f1c8aee-370f-4ae2-876d-a1f8893cd174.jpeg</url>
      <title>Forem: Tun</title>
      <link>https://forem.com/stereosky</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/stereosky"/>
    <language>en</language>
    <item>
      <title>Real-Time Infrastructure for Data Scientists</title>
      <dc:creator>Tun</dc:creator>
      <pubDate>Tue, 18 Jul 2023 10:19:19 +0000</pubDate>
      <link>https://forem.com/stereosky/real-time-infrastructure-for-data-scientists-28n1</link>
      <guid>https://forem.com/stereosky/real-time-infrastructure-for-data-scientists-28n1</guid>
      <description>&lt;p&gt;In our ongoing series on friction in feature engineering, we talked generally about the &lt;a href="https://quix.io/blog/bridging-the-impedance-gap/"&gt;impedance gap between data scientists and engineers&lt;/a&gt; and did a deep dive into the &lt;a href="https://quix.io/blog/feature-engineering-language-problem/"&gt;hassle of translating Python into Java&lt;/a&gt;. Here, I want to do another deep dive, but this time on the subject of infrastructure—another source of friction when getting real-time feature transformations into production.&lt;/p&gt;

&lt;p&gt;Here are the key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers and data scientists both need to deal with infrastructure—but they need different tools.&lt;/li&gt;
&lt;li&gt;Love it or hate it, the demand for “T-shaped” data scientists isn’t going away.&lt;/li&gt;
&lt;li&gt;When it comes to data pipelines, data scientists are used to the “plan and run” approach.&lt;/li&gt;
&lt;li&gt;When moving from batch to real-time, it helps to think in a service-oriented way.&lt;/li&gt;
&lt;li&gt;There are ML and real-time pipeline tools that support both approaches: Metaflow, Bytewax, Confluent Stream Designer and Quix.&lt;/li&gt;
&lt;li&gt;Each tool is strong in different areas such as usability, configurability, scalability and performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article could be considered a follow-up to Chip Huyen’s hotly-debated post, &lt;em&gt;&lt;a href="https://huyenchip.com/2021/09/13/data-science-infrastructure.html"&gt;Why data scientists shouldn’t need to know Kubernetes&lt;/a&gt;&lt;/em&gt;. The discussion around the post is almost as educational as the post itself.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://twitter.com/chipro/status/1437604700115935233"&gt;Twitter&lt;/a&gt; and &lt;a href="https://news.ycombinator.com/item?id=28649508"&gt;Hacker News&lt;/a&gt;, debates emerged about how much of the stack you need to learn. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There were data scientists who felt validated, shunning the expectation that they should be “full stack”.&lt;/li&gt;
&lt;li&gt;There were even developers who admitted that they don’t like using Kubernetes either, preferring to focus on their application logic. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the other hand, some commentators seemed less sympathetic and felt that data scientists should just “suck it up”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Xn7pphQo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/msb772uwf5403syvwy81.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Xn7pphQo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/msb772uwf5403syvwy81.png" alt="Image description" width="789" height="254"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One of the more &lt;a href="https://news.ycombinator.com/item?id=28653098"&gt;passionate comments&lt;/a&gt; in HN.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Of course, it’s not as black and white as the discussion would lead you to believe.&lt;/p&gt;
&lt;h2&gt;
  
  
  The spectrum between “no autonomy” and “complete autonomy”
&lt;/h2&gt;

&lt;p&gt;There is a range of skills that put a data scientist somewhere between “no autonomy” (getting an engineer to configure and deploy everything for you) and “complete autonomy” (writing your own Helm charts and Terraform modules). It’s unrealistic to expect a data scientist to be on the “complete autonomy” end of the spectrum, but it’s fair to expect them to move as far as possible towards it, especially if you’re in a small team. &lt;/p&gt;

&lt;p&gt;This expectation is common in the startup world; roles are usually more fluid and employees are expected to take on a wider array of responsibilities. This is why data scientists at startups tend to perform more data engineering tasks and spend less time on “pure” data science. &lt;/p&gt;

&lt;p&gt;This loosely mirrors the DevOps trend for developers, where developers became responsible for deploying their own code and relied less on SREs or infrastructure specialists. Yet, the comparison isn’t entirely fair—doing data science isn’t the same as developing applications. For one, the developer learns how to replicate the production environment on their machine from early on and frequently tests their code in staging environments that are exact replicas of production. This pattern hasn’t been as easy for data scientists to copy.&lt;/p&gt;
&lt;h2&gt;
  
  
  Data scientists and developers have different ways of working
&lt;/h2&gt;

&lt;p&gt;To understand the difference between these two roles, it helps to look at another scenario (just like I did in my &lt;a href="https://quix.io/blog/feature-engineering-language-problem/"&gt;language problem article&lt;/a&gt;). &lt;/p&gt;

&lt;p&gt;Suppose I have two services that both take stock trading data as input. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One is a typical service that plots the price movement of a stock and sends its app users alerts when the price has crossed a certain threshold in a specific direction.&lt;/li&gt;
&lt;li&gt;The second is an online feature transformation service that provides fresh features to an ML model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ML model is trained on patterns in historical trading activity coupled with real-time data. It looks at the trading behaviour in short time windows such as the last hour or last minute and gives users continually updated projections on a stock's movement within the current day as well as the long term (this is the same scenario I used in my last article). This is why it relies on the online feature transformation service.&lt;/p&gt;

&lt;p&gt;You could deploy both services using a similar pattern but there will need to be key differences in the workflow. To understand what I mean let’s look at how you would deploy a typical service.&lt;/p&gt;
&lt;h2&gt;
  
  
  How a developer might deploy a typical service
&lt;/h2&gt;

&lt;p&gt;Let’s say we’re using &lt;a href="https://aws.amazon.com/fargate/"&gt;AWS Fargate&lt;/a&gt; to deploy it. AWS Fargate is a serverless compute engine for containers that simplifies container deployment and management by automatically handling infrastructure provisioning, scaling and maintenance. This is what makes it so popular with developers.&lt;/p&gt;

&lt;p&gt;Having said that, the steps to deploy a service are still fairly complex. For example, look at the steps involved in the tutorials, &lt;em&gt;&lt;a href="https://medium.com/@thehouseofcards/how-to-deploy-a-python-microservice-on-fargate-part-2-93fcdb483372"&gt;How to deploy a Python Microservice on Fargate&lt;/a&gt;&lt;/em&gt; (CLI-based) or &lt;em&gt;&lt;a href="https://dev.to/aws-builders/deploy-microservices-on-aws-ecs-fargate-serverless-16el"&gt;Deploy Microservices on AWS ECS with Fargate&lt;/a&gt;&lt;/em&gt; (UI-based).&lt;/p&gt;

&lt;p&gt;Here’s a very rough summary of what you would need to do when starting a new project. &lt;/p&gt;
&lt;h4&gt;
  
  
  Preparation
&lt;/h4&gt;

&lt;p&gt;First, you would need to have a full development environment set up with Docker installed as well as the AWS and ECS CLIs.&lt;/p&gt;
&lt;h4&gt;
  
  
  Dockerise the application
&lt;/h4&gt;

&lt;p&gt;Then, after you’ve finished coding the logic for your service, you would need to Dockerise it. This means writing a Docker file that defines the entry point, software dependencies, relevant ports to open and so on. Then you run a build script to actually build a Docker image.&lt;/p&gt;
&lt;h4&gt;
  
  
  Push the image to a container registry
&lt;/h4&gt;

&lt;p&gt;Then you’d push the image to Amazon Elastic Container Registry (ECR). In this step you need to make sure that you have the correct IAM roles and ensure that your image is tagged correctly.&lt;/p&gt;
&lt;h4&gt;
  
  
  Deploy the image to your cluster as a service
&lt;/h4&gt;

&lt;p&gt;This is perhaps the most tricky part of the process. Here you need to define task and service definitions. This involves configuring execution roles, auto-scaling behaviour, load balancing, network settings and inter-service communication. Then you would rinse and repeat for the other services involved in your project.&lt;/p&gt;

&lt;p&gt;All of this requires some fairly in-depth knowledge of how infrastructure works in AWS and many development teams have a dedicated DevOps specialist to help them navigate the intricacies of networking and access management.&lt;/p&gt;

&lt;p&gt;However, once it is all set up, it’s usually automated with Infrastructure as Code (IaC) tools such as Terraform. This enables an automated CI/CD process to take care of deploying new versions of a service whenever developers push major changes to the underlying code. However, configuring a tool like Terraform is also &lt;a href="https://engineering.finleap.com/posts/2020-02-20-ecs-fargate-terraform/"&gt;not a trivial task&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Anyway, that’s a sketch of the process for a typical back-end developer. Let’s turn to the other type of task, one that is typically the concern of data scientists.&lt;/p&gt;
&lt;h2&gt;
  
  
  How an ML engineer might deploy an online feature transformation service
&lt;/h2&gt;

&lt;p&gt;Assuming you’re using the same infrastructure as the typical microservice (AWS Fargate), the deployment steps probably wouldn’t change much. However, you would need to update your network settings to ensure that the container has access to the wider internet (for example, access the Coinbase WebSocket feed to read fresh trading data).&lt;/p&gt;

&lt;p&gt;Secondly, you would need to connect the container to an online, in-memory data store such as Redis. This provides the feature transformation service with a place to write the calculated features. &lt;/p&gt;

&lt;p&gt;Thus, when the ML model needs to make a prediction, it fetches the most recent features from Redis. This allows the prediction service to access feature data at very high speed, which can be crucial for latency-sensitive applications.&lt;/p&gt;

&lt;p&gt;This of course begs the question—do we really expect data scientists to know all this too? Probably not. Most big companies have ML engineers to do it for them. And in companies that have batch-only machine learning processes, i.e. most companies, data scientists don’t have to think about application infrastructure at all. &lt;/p&gt;

&lt;p&gt;However, many data scientists do have to provision infrastructure for model training and run data pipelines internally. Internal pipelines also have their fair share of infrastructural wrangling, but these processes generally are “plan and run” workflows which have a start and end. This reduces the complexity to a small degree, but it’s still typically handled by a data engineer or a data scientist with a T-shaped skill set, i.e. they have acquired some data engineering skills.  &lt;/p&gt;

&lt;p&gt;It takes another set of skills to deploy a service that runs online and to automate and test that deployment. This is why there’s usually a handover process where data scientists pass their work to software or data/ML engineers to deploy online. However, this handover can be a bottleneck as we covered in our earlier article on the &lt;a href="https://quix.io/blog/bridging-the-impedance-gap/"&gt;impedance gap&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The demand for T-shaped data scientists
&lt;/h2&gt;

&lt;p&gt;Let’s revisit that notion of the T-shaped skill set for a moment. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zWvLbw5L--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l1um3brj35wvxcss317b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zWvLbw5L--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l1um3brj35wvxcss317b.png" alt="Image description" width="488" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The batch processing world is now full of them. Just search on LinkedIn for “data scientist” and you’ll often see “/ data engineer” tacked on to the end. Why is that? &lt;/p&gt;

&lt;p&gt;An article from early 2021 might give us a clue. In his article, &lt;em&gt;&lt;a href="https://www.mihaileric.com/posts/we-need-data-engineers-not-data-scientists/"&gt;We Don't Need Data Scientists, We Need Data Engineers&lt;/a&gt;&lt;/em&gt;, engineer Mihail Eric claimed that there were supposedly 70% more open roles for data engineers than for data scientists (out of the Y Combinator portfolio companies that he studied). His takeaway point was that the industry should place more emphasis on engineering skills when training data professionals. &lt;/p&gt;

&lt;p&gt;Also, given the lack of data engineers at the time he was writing, many lone data scientists acquired some engineering skills out of necessity. Initially, many companies only hired one data scientist, usually their first data hire, so they had to learn how to provision their own infrastructure to some degree (I have data scientist friends that have been in this position and conveyed their pain). Later, they were also aided by the emergence of new tools that simplified some infrastructural aspects of running an ML data pipeline.&lt;/p&gt;

&lt;p&gt;In the following section, I’ll take a look at one of these tools and identify some paradigms that are being carried over to tools for real-time data pipelines. The goal is to show how productivity gains from the batch world can also be applied to the real-time world.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rise of the “Plan and Run” approach
&lt;/h2&gt;

&lt;p&gt;In the batch world, a pipeline is bounded—there is a clear start and end. You run the pipeline and at some point it is done (until it is triggered again). In contrast, a real-time unbounded pipeline is usually never done, it runs continuously for eternity (unless you stop it or it encounters a serious error).&lt;/p&gt;

&lt;p&gt;The bounded nature of batch processing lends itself to being orchestrated as a “plan and run” approach. This is the approach used by workflow tools such as Dagster and Airflow. Both of these tools are designed to connect to a data integration platform such as Airbyte and run some data processing steps in sequence. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0MuTEXNH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zf3wf0gxqq6v2ld9xfzf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0MuTEXNH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zf3wf0gxqq6v2ld9xfzf.png" alt="Image description" width="800" height="508"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Screenshot from the &lt;a href="https://github.com/dagster-io/quickstart-etl"&gt;Dagster quick start&lt;/a&gt; for a job that fetches data from Hacker News’ APIs, transforms the collected data using Pandas and creates a word cloud based on trending stories (to visualise popular topics).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This processing workflow is written locally in a Python workflow file and then submitted to the workflow tool to run in a specific compute environment (hence “plan and run”). However, the disadvantage of these tools was that you still had to provision the infrastructure where the processing code would actually execute. This problem is especially acute when provisioning the resources to train memory-hungry machine learning models.&lt;/p&gt;

&lt;p&gt;To help solve this problem, Netflix open-sourced &lt;a href="https://metaflow.org/"&gt;Metaflow&lt;/a&gt;—originally an internal tool that abstracts away much of the infrastructure configuration and allows data scientists to use the same code in both development and production environments. The project was spun out into a separate startup called Outerbounds, which now manages its development. They also offer a &lt;a href="https://outerbounds.com/blog/announcing-outerbounds-platform/"&gt;managed platform&lt;/a&gt; which runs Metaflow so you don’t have to worry about setting it up.&lt;/p&gt;

&lt;p&gt;In any case, it has some great concepts that can be applied to real-time data pipelines too, so let's take a closer look.&lt;/p&gt;
&lt;h2&gt;
  
  
  Metaflow: infrastructure for ML, AI and data science
&lt;/h2&gt;

&lt;p&gt;You can run Metaflow locally, deploy it to external compute clusters, or use it in the Outerbounds managed platform. Once you have it set up, you can define and run your workflows in a cloud IDE. Unsurprisingly, you define your workflow in a Python file, which consists of steps that are defined with “&lt;a class="mentioned-user" href="https://dev.to/step"&gt;@step&lt;/a&gt;” decorators. You can also visualise your workflow as a DAG which illustrates how the workflow will be executed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4Wh3sZr9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hm99l2tw9fkn3an4f1gp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4Wh3sZr9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hm99l2tw9fkn3an4f1gp.png" alt="Image description" width="800" height="697"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real kicker though, is the ability to provision infrastructure for each workflow step. Here’s a very simple example, from the Metaflow sandbox.&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="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;kubernetes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;catch&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;memory_hog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Requesting a lot of memory"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bytes_used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1_000_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Success!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example shows how you can provision a container with a specific amount of memory that goes beyond the defaults. Unlike Airflow, Metaflow allows you to easily provision different kinds of containers for different steps so that each step has the resources that it needs. For more information on how the Kubernetes configuration works, see Metaflow’s documentation of the &lt;a href="https://docs.metaflow.org/api/step-decorators/kubernetes"&gt;“@kubernetes” decorator&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Although Metaflow is great for training and retraining models it’s not ideal for a real-time pipeline that runs continuous feature computations in production. &lt;/p&gt;

&lt;p&gt;Yet, there are other tools that borrow the same “plan and run” approach for real-time processing. Some key examples are Bytewax and Confluent’s Stream Designer. Let’s look at Bytewax first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bytewax: Timely Dataflows
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://bytewax.io/"&gt;Bytewax&lt;/a&gt; is a Python native binding to the Rust implementation of the Timely Dataflow library. Timely Dataflow was first introduced as a concept in the 2013 Microsoft Research paper “Naiad: A Timely Dataflow System”. The original Rust implementation is described as a distributed “data-parallel compute engine” that allows you to develop and run your code locally and then easily scale that code to multiple workers or processes without changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--g5C3iL0Z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4w0dq013mrt3bbryuflw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--g5C3iL0Z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4w0dq013mrt3bbryuflw.png" alt="Image description" width="556" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bytewax thus inherits all of these parallel processing features while providing a more accessible syntax and simplified programming interface, as well as a powerful CLI that lets you deploy a dataflow to an instance in the cloud.&lt;/p&gt;

&lt;p&gt;In its most basic form, a dataflow looks like this, where each step calls a different function, starting with an input and ending with an output.&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="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;flow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Dataflow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"inp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FileInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wordcount.txt"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# Take a line from the file
&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Lowercase all characters in the line
&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flat_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Split the line into words
&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initial_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Count the occurrence of each word in the file
&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reduce_window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sum"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clock_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Use a tumbling window emit the accumulated value every 5 seconds
&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"out"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StdOutput&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c1"&gt;# Output to console
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Example taken from the &lt;a href="https://bytewax.io/docs/getting-started/simple-example"&gt;Bytewax documentation&lt;/a&gt; (functions omitted for brevity).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Dataflows can also contain subflows nested within a function and it’s possible to run steps concurrently and pass different parameters to various steps. This allows you to run the same flow with different variables (for example, a certain price threshold or field name).&lt;/p&gt;

&lt;p&gt;Here’s how you would pass a threshold value of 10 to a flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; bytewax.run &lt;span class="s2"&gt;"parametric:get_flow(10)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sadly, you can’t define your infrastructure requirements in the code of the flow itself (like you can with Metaflow). However, you can (to a degree) define it at the command level.&lt;/p&gt;

&lt;p&gt;For example, suppose that you want to run individual processes on different machines in the same network. Specifically, you want to run 2 processes, with 3 workers each on two different machines. The machines are known in the network as &lt;em&gt;cluster_one&lt;/em&gt; and &lt;em&gt;cluster_two&lt;/em&gt;. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You would run the first process on cluster_one as follows:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; bytewax.run simple:flow &lt;span class="nt"&gt;---workers-per-process&lt;/span&gt; 3 &lt;span class="nt"&gt;--process-id&lt;/span&gt; 0 &lt;span class="nt"&gt;---addresses&lt;/span&gt; &lt;span class="s2"&gt;"cluster_one:2101;cluster_two:2101"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;And the second process like this:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; bytewax.run simple:flow &lt;span class="nt"&gt;---workers-per-process&lt;/span&gt; 3 &lt;span class="nt"&gt;---process-id&lt;/span&gt; 1 &lt;span class="nt"&gt;---addresses&lt;/span&gt; &lt;span class="s2"&gt;"cluster_one:2101;cluster_two:2101"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To deploy a dataflow to a Kubernetes cluster you would use the &lt;a href="https://bytewax.io/docs/deployment/waxctl"&gt;Waxctl&lt;/a&gt; tool which is Bytewax's equivalent to Kubernete's &lt;a href="https://kubernetes.io/docs/reference/kubectl/"&gt;kubectl&lt;/a&gt; client.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;waxctl dataflow deploy my-script.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cluster &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--processes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this introduces a little more complexity as you’re required to configure communication between the different workers yourself. So for example, if you have a cluster that runs &lt;a href="https://redpanda.com/"&gt;Redpanda&lt;/a&gt; or &lt;a href="https://kafka.apache.org/"&gt;Apache Kafka&lt;/a&gt; to store the output of intermediate steps, you’ll have to configure your workers to connect to Redpanda or Kafka.&lt;/p&gt;

&lt;p&gt;When you use the Waxctl CLI to deploy a Bytewax dataflow to a Kubernetes cluster, it will create the following components within the cluster.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QEOSQPUy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nf3e6y3lkve5l2z8q29u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QEOSQPUy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nf3e6y3lkve5l2z8q29u.png" alt="Image description" width="530" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These components are explained in more detail in the &lt;a href="https://bytewax.io/docs/deployment/k8s-ecosystem"&gt;Bytewax documentation&lt;/a&gt;, but what's important to point out there is that Bytewax will provision multiple replicas for your dataflow (indicated by my-dataflow-0, my-dataflow-1, etc.) based on your configuration settings. However, it’s not entirely clear how to allocate more resources to an especially memory-hungry step while letting the other steps run in low-resource containers. My guess is that you need to &lt;a href="https://github.com/bytewax/helm-charts"&gt;customise the Bytewax Helm chart&lt;/a&gt; first.&lt;/p&gt;

&lt;p&gt;As it stands, Bytewax definitely doesn't have the same level of configurability as Metaflow and there’s no managed version—you have to set up your own cluster on AWS or GCP. However, as I’ve pointed out before, they are currently working on a &lt;a href="https://bytewax.io/platform"&gt;managed platform&lt;/a&gt; which I am hopeful will follow in the steps of Metaflow and abstract away more of the infrastructure headache.&lt;/p&gt;

&lt;p&gt;Thus, from the perspective of a data scientist, Bytewax is fantastic for defining and orchestrating workflows but there's still the requirement of provisioning the accompanying infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confluent Stream Designer: real-time streaming pipelines with ksqlDB
&lt;/h2&gt;

&lt;p&gt;Confluent’s &lt;a href="https://docs.confluent.io/cloud/current/stream-designer/overview.html"&gt;Stream Designer&lt;/a&gt; is another tool with a “plan and run” approach to real-time pipelines. It uses ksqlDB as its processing engine and offers both a visual pipeline designer and a simple IDE for defining the pipeline in KSQL.&lt;/p&gt;

&lt;p&gt;Unlike Metaflow and Bytewax, Stream Designer does not allow you to provision different resources for different steps in the pipeline. This is by design. The entire pipeline is designed to run on a single ksqlDB cluster which handles all the data processing and state management. The processing load is divided amongst the available nodes in the cluster and if a node is added or removed, ksqlDB will automatically rebalance the processing workloads.&lt;/p&gt;

&lt;p&gt;Let’s look at an example of a pipeline which performs the following tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the sample Datagen source connector to get basic page view data. This is how the data will look:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"viewtime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1702311&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"userid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User_5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pageid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Page_39"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"viewtime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1702411&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"userid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User_6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pageid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Page_66"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"viewtime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1702541&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"userid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User_6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pageid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Page_89"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Write that data to a stream in a topic called “pageviews_topic”.&lt;/li&gt;
&lt;li&gt;Filter the stream for a specific user ID.&lt;/li&gt;
&lt;li&gt;Write the filtered stream back to a downstream topic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a screenshot of the finished pipeline that has been wired together in the Stream Designer UI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--e3aOAFgB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pa9b5kbx926kfrzui6hm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--e3aOAFgB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pa9b5kbx926kfrzui6hm.png" alt="Image description" width="800" height="113"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Although the UI is designed to abstract away the process of creating KSQL by hand, there are some parts of the configuration that require you to know a bit of KSQL—such as defining column names for a stream (sadly none of the fields have autocomplete, or defaults that might help a data scientist configure the steps faster). This is perhaps not possible because a pipeline is designed offline. It doesn’t yet know about some attributes of the online data. &lt;/p&gt;

&lt;p&gt;The following screenshot shows how you define columns for a filter stream:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3ahLGEAP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qgtvlncgeqogjhpxsznk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3ahLGEAP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qgtvlncgeqogjhpxsznk.png" alt="Image description" width="798" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you know a bit of KSQL, it is probably faster to write the steps by hand which you can also do by clicking “View pipeline graph and source”. This ability to easily switch back and forth between the code and the visual graph is a nice touch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--CuiLvpTn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wymzdt0bwsvqihequ73s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--CuiLvpTn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wymzdt0bwsvqihequ73s.png" alt="Image description" width="800" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you have written your pipeline, you click “activate” to deploy the whole pipeline at once. This makes it very much in line with the “plan and run” approach adopted by Metaflow and Bytewax. However, you can also add new components to an already running pipeline. If you want to change existing components, you need to deactivate the pipeline first and reactivate it when you’re done. &lt;/p&gt;

&lt;p&gt;One obvious weakness here though, is that workflows are based on KSQL rather than Python. We’ve already covered the &lt;a href="https://quix.io/blog/drawbacks-ksqldb-ml-workflows/"&gt;limitations of ksqlDB&lt;/a&gt; elsewhere, especially when it comes to machine learning. The gist of the article being that if you’re doing real-time machine learning and need to compute fresh features with complex transformations, it might not be the best choice.&lt;/p&gt;

&lt;p&gt;If you're not doing machine learning and your pipeline consists of fairly standard processing steps (i.e. filtering, joining, aggregating), Stream Designer is a great option. For data scientists, the infrastructure problem is taken care of because everything runs on a managed cluster, which has likely already been set up by your infrastructure or Confluent support team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quix: serverless data pipelines
&lt;/h2&gt;

&lt;p&gt;Last but not least, we have our own offering which uses many of the paradigms from the tools above. One difference though is that it works in a serverless manner. You don’t have to follow the “plan and run” approach (unless you really want to).&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://quix.io/signup"&gt;Quix platform&lt;/a&gt; has a visual graph which is very similar to Confluent Stream Designer, however, the graph itself can’t be edited in the same way as Stream Designer—you can’t wire together nodes because it is designed to represent services that have already been deployed. There is also currently no offline view like you would get in Confluent’s Stream Designer. However, you can interact with the graph by clicking different nodes and opening their relevant contextual menus.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--suJ39XuM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ejhj07dhxak8eq8ilc7w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--suJ39XuM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ejhj07dhxak8eq8ilc7w.png" alt="Image description" width="800" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each individual node represents a deployed project running a specific step in the pipeline. When you click on a node you can access the deployment settings that are used to run its process. You can also view the code—stored in your Git repo—that is running on the node (under the hood, this is handled by one or more Docker containers running in Kubernetes).&lt;/p&gt;

&lt;p&gt;In Quix, the infrastructure and workflow are decoupled from the processing logic. This means that the processing code is stored separately from the workflow (rather than one big long flow.py as it would be in Metaflow ) and they all reside in the same Git repository.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The infrastructure settings and workflow steps are written to a YAML file.&lt;/li&gt;
&lt;li&gt;The processing logic is stored in Python files and committed to the Git repository that you’ve defined for your workspace.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s an example of how the infrastructure and workflow logic is defined in YAML.&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="c1"&gt;# Quix Project Descriptor&lt;/span&gt;
&lt;span class="c1"&gt;# This file describes the data pipeline and configuration of resources of a Quix Project.&lt;/span&gt;

&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.0&lt;/span&gt;

&lt;span class="c1"&gt;# This section describes the Deployments of the data pipeline&lt;/span&gt;
&lt;span class="na"&gt;deployments&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;ADS Sim&lt;/span&gt;
    &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AdsSim&lt;/span&gt;
    &lt;span class="na"&gt;deploymentType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Job&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1.2&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4000&lt;/span&gt;
      &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8600&lt;/span&gt;
      &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;variables&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;output&lt;/span&gt;
        &lt;span class="na"&gt;inputType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OutputTopic&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;This is the output topic for demo sine wave data&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;f1-data&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;stream_id&lt;/span&gt;
        &lt;span class="na"&gt;inputType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FreeText&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;car1&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;FeaturesEng&lt;/span&gt;
    &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FeaturesEng&lt;/span&gt;
    &lt;span class="na"&gt;deploymentType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;features-v1.0&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
      &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;
      &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;desiredStatus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Stopped&lt;/span&gt;
    &lt;span class="na"&gt;variables&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;input&lt;/span&gt;
        &lt;span class="na"&gt;inputType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;InputTopic&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Name of the input topic to listen to.&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;f1-data&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;output&lt;/span&gt;
        &lt;span class="na"&gt;inputType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OutputTopic&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Name of the output topic to write to.&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;f1-data-features&lt;/span&gt;

&lt;span class="c1"&gt;# This section describes the Topics of the data pipeline&lt;/span&gt;
&lt;span class="na"&gt;topics&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;f1-data-features&lt;/span&gt;
    &lt;span class="na"&gt;persisted&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;partitions&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;replicationFactor&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;retentionInMinutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;-1&lt;/span&gt;
      &lt;span class="na"&gt;retentionInBytes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;262144000&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;f1-data&lt;/span&gt;
    &lt;span class="na"&gt;persisted&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;partitions&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;replicationFactor&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;retentionInMinutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;-1&lt;/span&gt;
      &lt;span class="na"&gt;retentionInBytes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;262144000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This YAML file is stored at the top level of your project. Like Stream Designer, the editing process is bidirectional. You can edit the YAML file directly (either locally or in the cloud IDE) or you can use the web UI to define the steps, which will be synced to the YAML file. Each step is called a “deployment” and is referenced by a unique name.&lt;/p&gt;

&lt;p&gt;Here’s an example of a deployment which contains the processing logic for a specific step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--i9p9p2Ea--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nek64hv2wnbk1z4b4g9c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--i9p9p2Ea--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nek64hv2wnbk1z4b4g9c.png" alt="Image description" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Generally, the Quix processing model is quite opinionated about how you store the data as it flows through your pipeline. Although not visible in the pipeline visualisation, each step typically outputs the data to a Kafka topic. &lt;/p&gt;

&lt;p&gt;Subsequent steps read from the Kafka topic and in turn produce their outputs to other Kafka topics as illustrated in the following diagram.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qd7z1py2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3122ywtc3mdriczkc9cs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qd7z1py2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3122ywtc3mdriczkc9cs.png" alt="Image description" width="800" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This means that the steps don’t need to be defined in any specific order in the YAML file. The order of steps can be determined from the topics that each step uses as input and output. Thus topics are like the links in a chain of steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary: comparing approaches
&lt;/h2&gt;

&lt;p&gt;Now that I’ve talked you through a few tools, let's compare their approaches. &lt;/p&gt;

&lt;h3&gt;
  
  
  Plan and Run approach
&lt;/h3&gt;

&lt;p&gt;The plan and run approach is fundamentally a paradigm from the batch processing world where a series of jobs are defined and executed all at once. This approach pushes you towards running the whole pipeline on one machine or container with a global set of resources. But this doesn’t work for machine learning workflows where some processing steps need far more resources than others. Thus, tools like Metaflow evolved to fill this gap. &lt;/p&gt;

&lt;p&gt;Similarly, Bytewax and Stream Designer both guide you to run your entire flows in one compute environment. In Stream Designer it’s simply not possible to do it any other way and in Bytewax the infrastructure learning curve is prohibitively complex for a data scientist to manage alone (although it does provide an easy way to scale horizontally).&lt;/p&gt;

&lt;p&gt;Thus, when it comes to provisioning infrastructure, Metaflow strikes the best balance between simplicity and configurability. Unfortunately, it’s not suitable for real-time pipelines because it doesn't have built-in functionality to handle streaming data or manage the lower latencies typically required in real-time systems.&lt;/p&gt;

&lt;p&gt;Bytewax looks more promising and it shows a lot of potential. For now, the better way to work with it would probably be to have a software engineer set up a staging environment for the data scientists and give them simple command line tools to deploy their dataflows with the required resources. When the code is ready, an engineer can deploy it to the production environment. Even with this setup, Bytewax adds a lot of value, helping to solve the &lt;a href="https://quix.io/blog/feature-engineering-language-problem/"&gt;code translation problem&lt;/a&gt; (thus cutting down time to production).&lt;/p&gt;

&lt;p&gt;Confluent’s Stream Designer looks like the simplest real-time solution for data scientists yet it’s not an easy option for data scientists who use ML models in their real-time pipelines. However, if Confluent were to release a version of Stream Designer for their new Apache Flink integration, that would certainly change the game (as long as they allowed you to write in Python as well as SQL).&lt;/p&gt;

&lt;h3&gt;
  
  
  Decoupled, service-oriented approach
&lt;/h3&gt;

&lt;p&gt;As I hinted at the start of the article, a decoupled, service-oriented approach is the default paradigm for any kind of application back-end (which is always inherently real-time in nature). That’s why I used AWS Fargate to describe how it works in typical software development. &lt;/p&gt;

&lt;p&gt;Online ML models and real-time feature computations are increasingly making their way into standard back-end architectures and are often deployed using the same approach. However, this leaves the data scientist with the difficult choice of trying to learn Docker and AWS Fargate or relying entirely on an ML engineer to get their code deployed. Most large companies go for the latter option, but in lean startups, the data scientist has to build up some infrastructure chops. In a sense, their job security depends on them becoming more T-shaped. As one Hacker News commenter &lt;a href="https://news.ycombinator.com/item?id=28656740"&gt;put it&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“The unfortunate truth the school of hard knocks has shown me is that someone without the "roll your sleeves up" attitude to learn Docker is generally speaking just not going to be that effective when push comes to shove. Now if you're using tools to abstract the time of data scientists who are CAPABLE of learning Docker, that is a different story.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Indeed this is one of the use cases that Quix was designed to address: empowering those data scientists who are capable of learning Docker but whose value is focusing on the processing logic (and algorithms). The Quix platform is intended to abstract away much of the infrastructure complexity associated with Docker and Kubernetes. This allows data scientists to develop, deploy and test their own code in staging environments within containerised services that interact with Kafka to produce and consume data. This encourages them to write better code because performance is often a trade-off based on the shape of the data. Better decisions for optimising their code will be made when they can actually see how system resources deal with production-level data volumes.&lt;/p&gt;

&lt;p&gt;In summary, tools like Bytewax and Stream Designer are a good fit if you’re already familiar with the plan and run approach and are making the first steps in moving from batch to real-time stream processing. The way you configure workflows is fairly similar to how you would do it in tools like Dagster and Airflow.&lt;/p&gt;

&lt;p&gt;If you have more complex real-time processing requirements where each step in your pipeline needs to be scaled separately, then Quix would be a superior choice to something like Fargate. If you have an army of machine learning engineers to handle the infrastructure, then by all means go for Fargate, or even build something in-house. But if you want to balance simplicity and configurability in the same manner as Metaflow, definitely give &lt;a href="https://quix.io/signup"&gt;Quix a try&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>dataengineering</category>
      <category>mlops</category>
    </item>
    <item>
      <title>Feature Engineering Has a Language Problem</title>
      <dc:creator>Tun</dc:creator>
      <pubDate>Tue, 04 Jul 2023 14:12:12 +0000</pubDate>
      <link>https://forem.com/stereosky/feature-engineering-has-a-language-problem-47bi</link>
      <guid>https://forem.com/stereosky/feature-engineering-has-a-language-problem-47bi</guid>
      <description>&lt;p&gt;Feature engineering is a crucial part of any machine learning (ML) workflow because it enables more complex models to be created than with raw data alone, but it's also one of the most difficult to manage. It's afflicted by a language barrier—a difference in the languages used to encode processing logic. To put it simply, data scientists define their feature computations in one language (e.g. Python or SQL) and data engineers often need to rewrite this logic in another language (e.g. Scala or Java).&lt;/p&gt;

&lt;p&gt;My colleague Mike touched on the reasons for this in a previous article "&lt;a href="https://quix.io/blog/bridging-the-impedance-gap/"&gt;Bridging the gap between data scientists and engineers in machine learning workflows&lt;/a&gt;", but I want to zoom in on what exactly this process entails and explore some ideas on how to remove some of the friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  When do teams encounter language friction?
&lt;/h2&gt;

&lt;p&gt;This problem starts to crop up as companies mature in their level of data sophistication. In-house ML isn't even worth considering until a company has a reliable data pipeline in place to supply models with training data.&lt;/p&gt;

&lt;p&gt;However, as data availability and data quality gradually improves, data teams start to create more sophisticated batch processing pipelines that incorporate machine learning. Machine learning models are trained offline and the outputs can begin as artifacts such as CSV files that are assessed by humans before progressing to other types such as class labels in the case of classification models.&lt;/p&gt;

&lt;p&gt;Feature transformations as well as training and inference pipelines written by data scientists usually aren't optimised for speed, so ML engineers often rewrite them to run faster. Rewriting the logic for feature engineering is the first place to look for performance gains.&lt;/p&gt;

&lt;p&gt;Once an offline ML pipeline has reached a stable state, many companies will look to leverage that data to enhance their product more directly. This often leads to ML models being integrated into application architectures so that, for example, web applications can adapt to customer requirements in real time.&lt;/p&gt;

&lt;p&gt;Thus, machine learning as a discipline needs to morph from being an experimental, sporadic, offline process into a repeatable software delivery process. Model files need to be deployed online and produce results in a timely manner. Likewise, the feature computation code from data scientists needs to be adapted for a production environment so the computations can run online. This enables the models to make predictions based on fresh features.&lt;/p&gt;

&lt;p&gt;It's at this stage when the impact of language friction starts to become a wider problem:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7z1sNZFK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o4ti17q45bbocxafmwla.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7z1sNZFK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o4ti17q45bbocxafmwla.png" alt="Image description" width="707" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rather than explain the theory and best practices behind feature engineering, I'd like to illustrate the language barrier with an example scenario.&lt;/p&gt;

&lt;h2&gt;
  
  
  An example scenario: feature engineering for AI-powered market predictions
&lt;/h2&gt;

&lt;p&gt;One of the most studied yet mysterious applications of machine learning is using it to predict the movement of certain financial markets. Since the predictions can influence the movement of the price, most organisations keep their prediction models under wraps. However, some trading apps are experimenting with some form of AI-powered prediction. This is especially prevalent in cryptocurrency trading where all trading data is publicly visible on the blockchain.&lt;/p&gt;

&lt;p&gt;For example, the SwissBorg trading app features an ML-powered "CyBorg Predictor" that forecasts price movements for certain assets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9CxcJ1mp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aksm77dr2g9hy9jbdq6k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9CxcJ1mp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aksm77dr2g9hy9jbdq6k.png" alt="Image description" width="800" height="288"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: &lt;a href="//swissborg.com"&gt;swissborg.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is a canonical example where real-time ML predictions can bring tangible business value (assuming the predictions are accurate!) so it lends itself nicely to an analysis of online feature engineering.&lt;/p&gt;

&lt;p&gt;So, let's say that you work for an up-and-coming crypto trading app that wants to introduce similar functionality.&lt;/p&gt;

&lt;p&gt;The key features that you need to train a machine learning model are the OHLC data points: the open, high, low and closing prices for a given time window. This data is typically visualised in the form of a candlestick chart which traders use for technical analysis.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7FIO94YL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7g9d3zp4edrzuzxsj3rx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7FIO94YL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7g9d3zp4edrzuzxsj3rx.png" alt="Image description" width="750" height="411"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: Sabrina Jiang © &lt;a href="https://www.investopedia.com/terms/o/ohlcchart.asp"&gt;Investopedia&lt;/a&gt; 2020.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are obviously services that provide precomputed OHLC data, but for argument's sake let's say you want to train a model on features that you've computed yourself. I want to walk through the process of taking this feature from an offline exploratory scenario to a real-time production scenario.&lt;/p&gt;

&lt;p&gt;Consequently, this scenario has two sections: prototype and production. Note that this is an oversimplification: in reality, there are more phases involved here (I highly recommend Chip Huyen's piece &lt;a href="https://huyenchip.com/2022/01/02/real-time-machine-learning-challenges-and-solutions.html"&gt;Real-time machine learning: challenges and solutions&lt;/a&gt; for more details). However, for the purposes of explaining the "language barrier", I want to keep things simple.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prototyping Offline with Python
&lt;/h2&gt;

&lt;p&gt;In the first iteration of your ML model, you might focus on one or two currencies such as ETH or Bitcoin. When prototyping the model, you might train the model offline on historical trading data and backtest it for prediction accuracy.&lt;/p&gt;

&lt;p&gt;Let's say your data scientist has a JSON file with some sample historical ticker data (it is ideally in the same JSON structure as data that will come from the live trading feed).&lt;/p&gt;

&lt;p&gt;Assume they're using Python for prototyping, they might first calculate ETH's 1-hour OHLC data like this:&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="nn"&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="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;

&lt;span class="c1"&gt;# Load raw ticker data from the JSON file
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'ticker_data.json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'r'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ticker_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Convert ticker data to a pandas DataFrame
&lt;/span&gt;&lt;span class="n"&gt;ticker_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="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticker_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Only keep rows with "product\_id" equals "ETH-USD"
&lt;/span&gt;&lt;span class="n"&gt;eth_usd_ticker_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ticker_df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ticker_df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"product_id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"ETH-USD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Convert the time column to pandas datetime
&lt;/span&gt;&lt;span class="n"&gt;eth_usd_ticker_df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'time'&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="n"&gt;to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eth_usd_ticker_df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'time'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Set the time column as the DataFrame index
&lt;/span&gt;&lt;span class="n"&gt;eth_usd_ticker_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eth_usd_ticker_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'time'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Calculate the OHLC data based on a 1-minute interval
&lt;/span&gt;&lt;span class="n"&gt;ohlc_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eth_usd_ticker_df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'price'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&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;resample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'1H'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'start'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"open"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"first"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"max"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"low"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"min"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"close"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"last"&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="c1"&gt;# Calculate the volume data based on a 1-minute interval
&lt;/span&gt;&lt;span class="n"&gt;volume_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eth_usd_ticker_df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'last_size'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&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;resample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'1H'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;'start'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Combine OHLC and volume data
&lt;/span&gt;&lt;span class="n"&gt;ohlc_volume_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="n"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ohlc_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;volume_df&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;axis&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="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ohlc_volume_df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script will partition the trading data into fixed 1-hour intervals resembling the following result.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;time&lt;/th&gt;
&lt;th&gt;open&lt;/th&gt;
&lt;th&gt;high&lt;/th&gt;
&lt;th&gt;low&lt;/th&gt;
&lt;th&gt;close&lt;/th&gt;
&lt;th&gt;last_size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;09/06/2023&lt;/td&gt;
&lt;td&gt;12:26:51.360251&lt;/td&gt;
&lt;td&gt;1846.55&lt;/td&gt;
&lt;td&gt;1846.56&lt;/td&gt;
&lt;td&gt;1846.01&lt;/td&gt;
&lt;td&gt;1846.55&lt;/td&gt;
&lt;td&gt;13.27384&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09/06/2023&lt;/td&gt;
&lt;td&gt;13:26:51.360251&lt;/td&gt;
&lt;td&gt;1846.53&lt;/td&gt;
&lt;td&gt;1846.53&lt;/td&gt;
&lt;td&gt;1846.22&lt;/td&gt;
&lt;td&gt;1846.22&lt;/td&gt;
&lt;td&gt;2.141272&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09/06/2023&lt;/td&gt;
&lt;td&gt;14:26:51.360251&lt;/td&gt;
&lt;td&gt;1864.99&lt;/td&gt;
&lt;td&gt;1864.99&lt;/td&gt;
&lt;td&gt;1864.68&lt;/td&gt;
&lt;td&gt;1864.68&lt;/td&gt;
&lt;td&gt;2.16268&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This data is OK for prototyping the ML model or providing batched long-term predictions, but not great for fine-grained real-time predictions. Prices can fluctuate wildly even within a 1-hour period so you'll want to catch those as they happen. This means putting the ML model online and combining the historical data with a stream of real-time trading data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculating Features Online with Java
&lt;/h2&gt;

&lt;p&gt;Now suppose that you have adapted the model to use features that are a combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1-hour intervals for the last 30 days.&lt;/li&gt;
&lt;li&gt;1-minute intervals for the current day.&lt;/li&gt;
&lt;li&gt;A sliding window of the last 60 seconds updating every second.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You want to put this model online so that it provides a stream of predictions that update in real time. These predictions might be used to populate a real-time dashboard or power automated trading bots.&lt;/p&gt;

&lt;p&gt;This requires the OHLC calculations to be refactored considerably. This refactoring is influenced by a number of factors that contribute to the so-called language barrier that slows down ML workflows.&lt;/p&gt;

&lt;p&gt;These factors are as follows:&lt;/p&gt;

&lt;h4&gt;
  
  
  Latency and throughput
&lt;/h4&gt;

&lt;p&gt;The query now needs to run on a continuous unbounded stream of data rather than a table. It also needs to maintain a specific rate of throughput to stop the predictions from getting stale. This requires a purpose-built stream-processing engine that can maintain throughput on high volumes of trading data.&lt;/p&gt;

&lt;p&gt;Apache Flink is one of the most popular choices for such use cases and although it supports SQL, many developers choose to write processing logic using Flink's lower-level APIs. Calculations run faster when accessing these APIs directly (rather than using an abstraction layer such as PyFlink or SQL).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Tuple5&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Tuple5&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Tuple5&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Tuple5&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;f0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                                   &lt;span class="c1"&gt;// Open (min)&lt;/span&gt;
        &lt;span class="nc"&gt;Math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;f1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;f1&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;                   &lt;span class="c1"&gt;// High&lt;/span&gt;
        &lt;span class="nc"&gt;Math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;f2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;f2&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;                   &lt;span class="c1"&gt;// Low&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;f3&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                                   &lt;span class="c1"&gt;// Close (latest value)&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;f4&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;f4&lt;/span&gt;                             &lt;span class="c1"&gt;// Volume&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;An excerpt of the math operations after refactoring in Flink.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Different dependencies
&lt;/h4&gt;

&lt;p&gt;If you're going to translate from SQL or Python into Java for Flink, then you'll also need to import different dependencies which need to be accessible in the execution environment. If you've created a custom function in the form of a UDF, you need to ensure that it is also packaged with the job and deployed to the Flink cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.flink.streaming.api.windowing.time.Time&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.flink.api.common.functions.AggregateFunction&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.flink.streaming.api.datastream.*&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.flink.streaming.api.environment.StreamExecutionEnvironment&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.flink.api.java.tuple.*&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.flink.streaming.api.windowing.windows.TimeWindow&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.flink.api.common.serialization.SimpleStringSchema&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.Properties&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;An excerpt of all the extra dependencies required after refactoring code into Java.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Real-time data sources and sinks
&lt;/h4&gt;

&lt;p&gt;To calculate OHLC data on a sliding window, the query now needs to use a different data source. Instead of connecting to a database and querying a table, the process needs to operate on some kind of message queue, which is typically a Kafka topic.&lt;/p&gt;

&lt;p&gt;Thus a lot of "connector code" needs to be added so that the process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connects to a Kafka message broker.&lt;/li&gt;
&lt;li&gt;Reads raw data from one topic and writes results to a second topic.&lt;/li&gt;
&lt;li&gt;Efficiently serialises and deserialises the data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also more connector code required to write the feature values themselves to an online feature store such as Redis.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create Kafka consumer properties&lt;/span&gt;
&lt;span class="nc"&gt;Properties&lt;/span&gt; &lt;span class="n"&gt;consumerProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Properties&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;consumerProps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bootstrap.servers"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"myserver:9092"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;consumerProps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"group.id"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"flink-ohlc-group"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Create Kafka producer properties&lt;/span&gt;
&lt;span class="nc"&gt;Properties&lt;/span&gt; &lt;span class="n"&gt;producerProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Properties&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;producerProps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bootstrap.servers"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"myserver:9092"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;A small excerpt of the extensive Kafka configuration required for Flink.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Windowed aggregations and state management
&lt;/h4&gt;

&lt;p&gt;In the prototyping phase, you might already start testing sliding window calculations, but you'd probably use an in-memory dictionary to store the state. This works fine on one computer. But moving to production, however, you would need to use a processing engine that maintains a shared state in a fault-tolerant manner. This is again why many companies choose Apache Flink which is famous for its reliable stateful processing in a distributed computing environment.&lt;/p&gt;

&lt;p&gt;If a replica of a process somehow terminates when it's in the middle of calculating OHLC data for a sliding window, another replica can come and pick up where the previous process left off because the calculation steps are continuously written to a shared storage location.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Calculate the OHLC data for each ticker over a 30-second sliding window&lt;/span&gt;
&lt;span class="nc"&gt;DataStream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Tuple5&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ohlcStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tickStream&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;keyBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ticker&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Group by ticker&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timeWindow&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;// Sliding window of 30 seconds with 1 second slide&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;aggregate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OhlcAggregator&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;An excerpt of a sliding window calculation using Flink's DataStream API in Java.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As you can see, that's a lot of refactoring. And I haven't even touched on other process changes such as adding the feature to a feature catalog, interacting with an online feature store, testing, deploying and monitoring the online feature calculation.&lt;/p&gt;

&lt;p&gt;But rewriting the code from top to bottom alone can slow down a feature's journey from prototype to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solutions to the language barrier
&lt;/h2&gt;

&lt;p&gt;If this problem is so ubiquitous, how do the big players solve it? It turns out that Netflix, Uber, DoorDash have all built their own sophisticated feature platforms that handle feature management as well as stream and batch processing. They still have the feature translation issue, but they're able to automate the translation process for common calculations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unified Feature Platforms
&lt;/h3&gt;

&lt;p&gt;The following table comes from another of Chip Huyen's brilliant pieces, this time "&lt;a href="https://huyenchip.com/2023/01/08/self-serve-feature-platforms.html"&gt;Self-serve feature platforms: architectures and APIs&lt;/a&gt;". It illustrates just how many proprietary custom-built feature platform features are out there in the wild already. Note that features are typically still defined in multiple languages.&lt;/p&gt;

&lt;h4&gt;
  
  
  Comparison of feature platforms
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Feature store&lt;/th&gt;
&lt;th&gt;Feature API &lt;em&gt;(transformation logic &amp;gt; feature logic)&lt;/em&gt;
&lt;/th&gt;
&lt;th&gt;Stream compute engine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/linkedin/venice"&gt;Venice&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=vksWF8UgWXc"&gt;Fedex&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Python &amp;gt; Python&lt;/td&gt;
&lt;td&gt;Samza, Flink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Airbnb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HBase-based&lt;/td&gt;
&lt;td&gt;Python &amp;gt; Python&lt;/td&gt;
&lt;td&gt;Spark Streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Instacart&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scylla, Redis&lt;/td&gt;
&lt;td&gt;? &amp;gt; YAML&lt;/td&gt;
&lt;td&gt;Flink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DoorDash&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Redis, CockroachDB&lt;/td&gt;
&lt;td&gt;SQL &amp;gt; YAML&lt;/td&gt;
&lt;td&gt;Flink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Snapchat/KeyDB"&gt;KeyDB&lt;/a&gt; (multithreaded fork of Redis)&lt;/td&gt;
&lt;td&gt;SQL &amp;gt; YAML&lt;/td&gt;
&lt;td&gt;Spark Streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stripe&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In-house, Redis&lt;/td&gt;
&lt;td&gt;Scala &amp;gt; ?&lt;/td&gt;
&lt;td&gt;Spark Streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Meta (FB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Scala-like &amp;gt; ?&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.youtube.com/watch?v=DNI54vc1ALQ"&gt;XStream&lt;/a&gt;, &lt;a href="https://github.com/facebookincubator/velox"&gt;Velox&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spotify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bigtable&lt;/td&gt;
&lt;td&gt;Flink SQL &amp;gt; ?&lt;/td&gt;
&lt;td&gt;Flink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Uber&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cassandra, DynamoDB&lt;/td&gt;
&lt;td&gt;DSL &amp;gt; ?&lt;/td&gt;
&lt;td&gt;Flink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lyft&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Redis, DynamoDB&lt;/td&gt;
&lt;td&gt;SQL &amp;gt; YAML&lt;/td&gt;
&lt;td&gt;Flink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pinterest&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In-house, memcached&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;Flink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Criteo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Couchbase&lt;/td&gt;
&lt;td&gt;SQL &amp;gt; JSON&lt;/td&gt;
&lt;td&gt;Flink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Binance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Flink SQL &amp;gt; Python&lt;/td&gt;
&lt;td&gt;Flink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Twitter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://blog.twitter.com/engineering/en_us/a/2014/manhattan-our-real-time-multi-tenant-distributed-database-for-twitter-scale"&gt;Manhattan&lt;/a&gt;, CockroachDB&lt;/td&gt;
&lt;td&gt;Scala&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/apache/incubator-heron"&gt;Heron&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gojek&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DynamoDB&lt;/td&gt;
&lt;td&gt;SQL &amp;gt; JSON&lt;/td&gt;
&lt;td&gt;Flink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Etsy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bigtable&lt;/td&gt;
&lt;td&gt;Scala &amp;gt; ?&lt;/td&gt;
&lt;td&gt;Dataflow&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Source: “&lt;a href="https://huyenchip.com/2023/01/08/self-serve-feature-platforms.html"&gt;Self-serve feature platforms: architectures and APIs&lt;/a&gt;" by Chip Huyen.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Yet not every company has the time or resources to build their own in-house feature platform. Now that more companies are moving into the later stages of the ML maturity model, there is increasing demand for simpler end-to-end solutions that help ease the language barrier while eliminating infrastructural complexity.&lt;/p&gt;

&lt;p&gt;There are now general feature platforms such as &lt;a href="https://www.tecton.ai/blog/what-is-a-feature-platform/"&gt;Tecton&lt;/a&gt; (proprietary) and &lt;a href="https://github.com/feathr-ai/feathr"&gt;Feathr&lt;/a&gt; (open source) which aim to keep the batch and streaming code tightly synchronised while handling the actual processing itself. This in itself is enough to cut down the time to production. When LinkedIn &lt;a href="https://engineering.linkedin.com/blog/2022/open-sourcing-feathr---linkedin-s-feature-store-for-productive-m"&gt;announced that they were open sourcing Feathr&lt;/a&gt; in April 2022, they revealed that it had "&lt;em&gt;reduced engineering time required for adding and experimenting with new features from weeks to days".&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Tecton goes further and removes the headache of having to provision extra infrastructure (assuming that you have Databricks, Amazon EMR, or Snowflake set up as an offline feature store). They provide an end-to-end platform for managing, storing and computing online and offline features.&lt;/p&gt;

&lt;p&gt;The following screenshot from Tecton should give you a rough idea of how these feature platforms work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gZ_awQgJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kts2hfzesn1egkyfzm9e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gZ_awQgJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kts2hfzesn1egkyfzm9e.png" alt="Image description" width="749" height="511"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: &lt;a href="https://www.tecton.ai/#feature-logic"&gt;tecton.ai&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You essentially store variants of the same feature transformation in one "entry" along with some configuration variables that affect the score of the transformation. Connections to external sources such as Kafka are defined elsewhere in Tecton's configuration, so there is a clean separation of concerns between the transformation code and the streaming transport code.&lt;/p&gt;

&lt;h4&gt;
  
  
  Caveats
&lt;/h4&gt;

&lt;p&gt;Such systems are still intended for companies who are fairly advanced in their ML maturity. They're in some ways, designed to prevent large enterprises from repeatedly building their own custom feature platforms (although many still do). For this reason, these platforms are still fairly complex, probably because they need to address the highly specific requirements of many enterprises with mature MLOps teams. If you're starting off with a limited feature set, there is a risk that the additional complexity could offset the time-savings that you gain by having a more structured feature management pipeline.&lt;/p&gt;

&lt;p&gt;The other issue is that they still use Spark or Flink under the hood to do stream processing, which means that code is still being translated or 'transpiled' at some level. Tecton, for example, uses Spark Structured Streaming for stream processing. Spark's native API is written in Scala, so as with Flink, the Python API is just a wrapper around the native API so using it can introduce extra latency. Additionally, Spark Structured Streaming uses a micro-batch processing model, which generally has higher latency compared to event-driven streaming systems like Apache Flink or Kafka Streams. It also lacks built-in complex event processing (CEP) features that other frameworks like Apache Flink offer.&lt;/p&gt;

&lt;p&gt;However, not every application requires CEP or very low-latency processing (sub-second or milliseconds), so in most cases the stream processors built into these feature platforms will do the job.&lt;/p&gt;

&lt;p&gt;But what if you want a simpler solution that gives you more direct control over the stream processing logic and while not requiring data scientists to grapple with Java or Scala? That's where the other type of solution comes into play—pure Python stream processing frameworks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pure Python stream processing frameworks
&lt;/h3&gt;

&lt;p&gt;A pure Python stream processing framework can enable data scientists to prototype with streaming data very early on in the process. They do so by making it very easy to connect to Kafka and run the typical operations that you would perform on an unbounded stream (i.e. sliding window aggregations). A data scientist might still build their logic on a batch dataset first, but it becomes very simple to adapt that same logic for streaming data. This reduces the language barrier, because the same prototype code can be used in production with very minimal refactoring.&lt;/p&gt;

&lt;p&gt;In an ideal scenario, the data scientists can also use Python to define the processing workflows. Many features need to be calculated in multiple steps, so it helps to give data scientists more autonomy in defining workflows as well as the transformation logic itself.&lt;/p&gt;

&lt;p&gt;For example, Faust and Bytewax are both pure Python stream processing frameworks that can be used in complex processing pipelines.&lt;/p&gt;

&lt;h4&gt;
  
  
  Faust
&lt;/h4&gt;

&lt;p&gt;Faust was open sourced by Robinhood in 2018 and has since been taken over by the open source community.&lt;/p&gt;

&lt;p&gt;When it was &lt;a href="https://robinhood.engineering/faust-stream-processing-for-python-a66d3a51212d"&gt;first released&lt;/a&gt;, Faust looked very promising. For example, Robinhood's engineering team published a compelling blog post on how they used Faust in combination with Apache Airflow to &lt;a href="https://robinhood.engineering/how-we-built-a-better-news-system-8c30b77067bc"&gt;build a better news system&lt;/a&gt;. They used Faust commands via Airflow to continuously pull data from various sources (such as RSS feeds and aggregators) while using Kafka to store the results of every processing step. Faust also supports scalable stateful processing with so-called "stateful tables" and can be configured for exactly once processing via the "&lt;a href="https://faust.readthedocs.io/en/latest/userguide/settings.html#processing-guarantee"&gt;processing_guarantee&lt;/a&gt;&lt;a href="https://faust.readthedocs.io/en/latest/userguide/settings.html#processing-guarantee"&gt;" setting&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, it appears that Robinhood has abandoned Faust. It's not clear why exactly, but there was &lt;a href="https://www.reddit.com/r/dataengineering/comments/ubzvnc/why_did_robinhood_abandon_faust/"&gt;plenty of speculation on Reddit&lt;/a&gt;. There is now a fork of Robinhood's original Faust repo which is more actively maintained by the open source community. However, it still has a lot of open bugs which are show-stoppers for some teams (see this &lt;a href="https://kapernikov.com/a-comparison-of-stream-processing-frameworks/"&gt;review of stream processing frameworks&lt;/a&gt; for more details on those bugs).&lt;/p&gt;

&lt;h4&gt;
  
  
  Bytewax
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://bytewax.io/"&gt;Bytewax&lt;/a&gt; is a lot newer, launched in early 2021 and open-sourced in February 2022, but is quickly gaining traction due to it being open source and very user-friendly for data scientists. Unlike Faust, Bytewax aims to be a complete stream processing platform and includes functionality to enable data scientists to build their own dataflows—in other words, processing pipelines that include multiple steps that can be represented as nodes in a graph.&lt;/p&gt;

&lt;p&gt;In fact, the example OHLC scenario I provided earlier was inspired by a tutorial that uses a simple Bytewax dataflow to read data from a Coinbase WebSocket and write the OHLC feature values to a feature store (Hopsworks).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BHTCo5od--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/68l9q6jnubb0ppxdn6ou.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BHTCo5od--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/68l9q6jnubb0ppxdn6ou.png" alt="Image description" width="800" height="554"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: "&lt;a href="https://paulabartabajo.substack.com/p/real-world-ml-019-deploy-a-real-time"&gt;Real-World ML #019: Deploy a real-time feature pipeline to AWS&lt;/a&gt;" by Pau Labarta Bajo.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Caveats
&lt;/h4&gt;

&lt;p&gt;Given that the official repo seems to be abandoned, the caveats with Faust should hopefully be clear. Although the Faust fork is more active, it's still uncertain when some of the more serious bugs are going to be fixed. It's worth noting that we also encountered these bugs when trying to do some benchmarking against Faust (for our own Python library).&lt;/p&gt;

&lt;p&gt;Bytewax is still fairly new so it will take a while for more reports about how it fares in production to trickle through the ecosystem. When it comes to deploying it, however, you'll still have to deal with some infrastructural complexity—at least for now (they have a managed platform in the works). Looking at their &lt;a href="https://www.bytewax.io/docs/deployment/container"&gt;deployment documentation&lt;/a&gt;, it's clear that they expect readers to have some knowledge of the infrastructure that will host the stream processing logic. You can choose to run dataflows in local Docker containers, in Kubernetes, AWS EC2 instances, or GCP VM instances. All of these require setup and configuration work that would probably be uninteresting to a data scientist and is probably better handled by a friendly (ML) engineer. Much of this complexity will hopefully go away once their platform becomes generally available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By now it should be clear the data and ML industry is well aware of the language barrier affecting feature engineering in real-time ML workflows. It has always existed, but was historically solved with in-house solutions hidden from the public. Real-time inference on real-time features was practised by a chosen few with highly specific requirements—so it made sense for them to build their own solutions. Now, with all the increased attention on AI, we're seeing a democratisation of many aspects of MLOps workflows and there are now more standardised approaches to tackling the language barrier such as all-in-one feature platforms and pure Python stream processing frameworks.&lt;/p&gt;

&lt;p&gt;Although I've focused on Faust and Bytewax, it would be remiss of me not to mention our own platform &lt;a href="https://quix.io/signup"&gt;Quix&lt;/a&gt; which runs &lt;a href="https://github.com/quixio/quix-streams"&gt;Quix Streams&lt;/a&gt;— our open source stream processing library. The processing model is not unlike that of Bytewax, but instead of defining data pipelines in Python, you use the Quix Portal UI to piece together your transformation steps (for a peek at how it works in production, see this &lt;a href="https://quix.io/blog/real-time-batch-analytics-connected-bikes-quix-aws/"&gt;telemetry case study&lt;/a&gt;). The Quix platform is also a fully hosted and managed solution that uses Kafka and Kubernetes under the hood—which makes it pretty much infinitely scalable. We aim to solve the language barrier in the same way as Faust and Bytewax but we want to remove the infrastructure headache too. However, infrastructure is a whole other subject which I hope to tackle in a follow-up post. For now, I hope that my simple example scenario has helped you understand the language barrier in more detail and inspired you to plan for it when you're ready to dive into real-time feature processing.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>dataengineering</category>
      <category>mlops</category>
    </item>
  </channel>
</rss>
