用Python和Pyodide构建单页应用程序 - 第二部分

503 阅读8分钟

在本系列的第一个教程中,我们使用Python和Pyodide构建了一个单页应用程序,以加载Pandas,获取Netflix数据集,并对数据进行基本计算。我们还看了Pyodide是如何被用来直接用Python操作DOM的。在我们构建的应用程序中,我们将处理好的Netflix数据传递给一个JavaScript组件,并直接从Python代码中呈现出来。

正如在第一部分的结论中提到的,这个应用程序缺少一些功能,我们需要解决一些问题。在这第二部分中,我们将。

  1. 用Pandas更好地分析和处理数据
  2. 使用网络工作者来加快应用程序的速度

--

Python单页应用程序系列。

  1. 第一部分。学习Pyodide的基础知识并创建基础应用程序
  2. 第二部分(本教程!)。用Pandas分析和处理数据,并使用网络工作者来加快应用的速度
  3. 第三部分(即将推出)。创建一个Python包,添加额外的功能,并添加一个持久的数据层。

目标

在本教程结束时,你应该能够。

  1. 使用Pandas的更多高级功能来分析和处理数据
  2. 使用网络工作者改善用户体验和性能

我们要做的是

首先,我们将通过使用网络工作者来改善用户体验和应用程序性能。我们还将深入研究用于分析和处理Netflix数据的Pandas库,以便根据给定的标题创建建议,并添加随机的电影和节目事实。

你可以在这里找到该应用程序的实时演示。

用潘达分析Netflix数据集

在第一部分中,在加载Netflix的CSV文件后,我们删除了一些不必要的列,并将结果以JSON格式返回。正如你所看到的,我们还没有对数据做太多的分析和处理。我们现在就来看看这个。

如果你需要第一部分的代码,你可以在这里找到它。

创建推荐列表

经过消毒的DataFrame有以下几列。

  • id
  • 标题
  • 版本_年份
  • 类型
  • 制作国家
  • 分数(imdb_score
  • 投票数
  • 录音录像带得分
  • tmdb_popularity

让我们用Pandas创建一个电影和节目的推荐列表。为此,我们将在DataFrame中添加一个新的列,称为recommendation_score ,其值是imdb_votesimdb_scoretmdb_scoretmdb_popularity 的加权和。

recommended_titles["recommendation_score"] = (
    sanitized_titles["imdb_votes"] * 0.3 +
    sanitized_titles["imdb_score"] * 0.3 +
    sanitized_titles["tmdb_score"] * 0.2 +
    sanitized_titles["tmdb_popularity"] * 0.2
)

在你选择的代码编辑器中打开index.html文件,并在后面添加以下代码titles_list = sanitized_titles.head(10).to_json(orient="records")

# 4. Create recommendation list for Shows and Movies
# 4.1 Copy the sanitized_titles to add new column to it
recommended_titles = sanitized_titles.copy()

# 4.2 Add new column to the sanitized_titles
recommended_titles["recommendation_score"] = (
    sanitized_titles["imdb_votes"] * 0.3 +
    sanitized_titles["imdb_score"] * 0.3 +
    sanitized_titles["tmdb_score"] * 0.2 +
    sanitized_titles["tmdb_popularity"] * 0.2
)
print(recommended_titles.head(5))

在你的浏览器中打开该文件。然后,在你的浏览器的开发者工具的控制台内,你应该看到前五个标题。请注意recommendation_score 列。

      id                            title  ... tmdb_score recommendation_score
 tm84618                      Taxi Driver  ...        8.2          238576.2524
tm127384  Monty Python and the Holy Grail  ...        7.8          159270.7632
 tm70993                    Life of Brian  ...        7.8          117733.1610
tm190788                     The Exorcist  ...        7.7          117605.6374
 ts22164     Monty Python's Flying Circus  ...        8.3           21875.3838

有了这个,让我们创建两个新的DataFrames,一个是电影,另一个是节目,然后按recommendation_score ,以降序排序。

recommended_movies = (
    recommended_titles.loc[recommended_titles["type"] == "MOVIE"]
    .sort_values(by="recommendation_score", ascending=False)
    .head(5)
    .to_json(orient="records")
)

recommended_shows = (
    recommended_titles.loc[recommended_titles["type"] == "SHOW"]
    .sort_values(by="recommendation_score", ascending=False)
    .head(5)
    .to_json(orient="records")
)

在这里,我们使用locsort_values方法,分别按type 列过滤标题,并按recommendation_score 降序排序。

用这些新的列表替换print(recommended_titles.head(5))

# 4. Create recommendation list for Shows and Movies
# 4.1 Copy the sanitized_titles to add new column to it
recommended_titles = sanitized_titles.copy()

# 4.2 Add new column to the sanitized_titles
recommended_titles["recommendation_score"] = (
    sanitized_titles["imdb_votes"] * 0.3 +
    sanitized_titles["imdb_score"] * 0.3 +
    sanitized_titles["tmdb_score"] * 0.2 +
    sanitized_titles["tmdb_popularity"] * 0.2
)
recommended_movies = (
    recommended_titles.loc[recommended_titles["type"] == "MOVIE"]
    .sort_values(by="recommendation_score", ascending=False)
    .head(5)
    .to_json(orient="records")
)
recommended_shows = (
    recommended_titles.loc[recommended_titles["type"] == "SHOW"]
    .sort_values(by="recommendation_score", ascending=False)
    .head(5)
    .to_json(orient="records")
)

为了在我们的应用程序中使用这些列表,首先我们需要在App's state中添加新的键,以便能够保存和操作数据。

state = {
  titles: [],
  recommendedMovies: [],
  recommendedShows: [],
}

现在,为了更新状态,在js.window.appComponent.state.titles = titles_list 后面添加以下内容。

js.window.appComponent.state.recommendedMovies = recommended_movies
js.window.appComponent.state.recommendedShows = recommended_shows

最后,为了向终端用户显示建议,在view() ,就在这一行下面添加以下内容。



  
  
    Recommended Movies
    
      ${this.state.recommendedMovies.length > 0 ? JSON.parse(this.state.recommendedMovies).map(function (movie) {
          return `
            
              
                
                  ${movie.title}
                  ${movie.description}
                
                ${movie.release_year}
              
            
            `
        }).join('') : `
          
            
              
                Loading...
              
            
          
        
        `
      }
    
    

    
    
      Recommended Shows
      
        ${this.state.recommendedShows.length > 0 ? JSON.parse(this.state.recommendedShows).map(function (show) {
          return `
            
              
                
                  ${show.title}
                  ${show.description}
                
                ${show.release_year}
                
              
              `
        }).join('') : `
          
            
              
                Loading...
              
            
          
        
      `}
    
    


回到你的浏览器中,你现在应该看到推荐的电影和节目了。

电影和节目事实

在这一节中,我们将找到产生最多电影和节目的年份,从Python代码开始。

# 5. Movie and Show Facts

facts_movies = (
    sanitized_titles.loc[sanitized_titles["type"] == "MOVIE"]
    .groupby("release_year")
    .count()["id"]
    .sort_values(ascending=False)
    .head(1)
    .to_json(orient="table")
)

facts_shows = (
    sanitized_titles.loc[sanitized_titles["type"] == "SHOW"]
    .groupby("release_year")
    .count()["id"]
    .sort_values(ascending=False)
    .head(1)
    .to_json(orient="table")
)

在这里,我们使用:

  1. groupby方法将标题按release_year
  2. count来计算每年的标题数量。
  3. sort_values对标题按每年的标题数量进行降序排序。

将上述代码添加到index.html文件中,就在建议部分的下面。

再次更新App's state。

state = {
  titles: [],
  recommendedMovies: [],
  recommendedShows: [],
  factsMovies: [],
  factsShows: [],
}

更新状态。

# 6. set titles to first 10 titles to the state, update remaining state, and render
js.window.appComponent.state.titles = titles_list
js.window.appComponent.state.recommendedMovies = recommended_movies
js.window.appComponent.state.recommendedShows = recommended_shows
js.window.appComponent.state.factsMovies = facts_movies   # NEW
js.window.appComponent.state.factsShows = facts_shows     # NEW
js.window.appComponent.render()

再次更新view() ,添加以下内容,就在.NET的后面。



  
    Interesting Facts
    
      
        ${this.state.factsMovies.length > 0 ?
          `
            Movies produced in ${JSON.parse(this.state.factsMovies).data[0].release_year}
            ${JSON.parse(this.state.factsMovies).data[0].id}
          ` : `
            Loading...
          `}
      
      
        ${this.state.factsShows.length > 0 ?
          `
            Shows produced in ${JSON.parse(this.state.factsShows).data[0].release_year}
            ${JSON.parse(this.state.factsShows).data[0].id}
          ` : `
            Loading...
          `}
      
    
  


在浏览器中重新加载index.html页面。你应该看到有趣的事实部分,其中包括在产生最多电影和节目的年份产生的电影和节目的数量。

提高性能

当前实现的一个问题是,我们把昂贵的操作放在浏览器的主线程中。这样做的后果是,在Pyodide完成加载和执行代码之前,其他操作会被阻断。这可能会对应用程序的性能和用户体验产生负面影响。

网络工作者

为了解决这个问题,我们可以使用Web Worker来卸载繁重的操作--在这种情况下,Pyodide和Python脚本--到后台的一个单独的线程中,让浏览器的主线程继续运行其他操作而不被拖慢或锁定。

网络工作者的主要组成部分是。

  1. Worker()构造函数。创建一个新的网络工作者实例,我们可以向其传递一个脚本,该脚本将在一个单独的线程中运行。
  2. onmessage()事件。当工作者收到来自另一个线程的消息时触发的。
  3. postMessage()方法。向工作者发送一条消息
  4. terminate()方法。终止工作器

让我们来看看一个简单的例子。

在您项目的根目录下创建一个名为worker.js的新文件。

self.onmessage = function(message) {
  console.log(message.data);
}

该文件包含工作者要运行的代码。

index.html 中创建一个新的script 标签,就在关闭的body 标签之前。

由于安全原因,网络工作者文件不能通过file:// 协议从您的本地文件系统导入。我们需要运行一个本地Web服务器来运行该项目。在你的终端中,导航到你项目的根目录。然后,运行 Python 的http 服务器

在服务器运行时,在你的浏览器中导航到http://localhost:8000/。你应该在开发者控制台中看到Hello from the main thread!

将Pyodide移到一个Web Worker上

我们在这一部分的目标是。

  1. 在 Web Worker 中加载并初始化 Pyodide 和它的包
  2. 在Web Worker中运行我们的Python脚本,并将结果发布到主线程,以便渲染它

首先,删除index.html中的函数定义和对main() 的调用。然后,将worker.js中的所有代码替换为。

// load pyodide.js
importScripts("https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js");

// Initialize pyodide and load Pandas
async function initialize(){
  self.pyodide = await loadPyodide();
  await self.pyodide.loadPackage("pandas");
}

let initialized = initialize();

现在,在worker*.*js文件中添加以下代码,以便在worker被初始化时运行我们的脚本。

self.onmessage = async function (e) {
  await initialized;
  response = await fetch(
    "https://raw.githubusercontent.com/amirtds/kaggle-netflix-tv-shows-and-movies/main/titles.csv"
  );
  response.ok && response.status === 200
    ? (titles = await response.text())
    : (titles = "");
  // define global variable called titles to make it accessible by Python
  self.pyodide.globals.set("titlesCSV", titles);
  let titlesList = await self.pyodide.runPythonAsync(`
    import pandas as pd
    import io

    # 1. create csv buffer to make it readable by pandas
    csv_buffer = io.StringIO(titlesCSV)
    # 2. load the csv file
    all_titles = pd.read_csv(csv_buffer)

    # 3. sanitize the data
    # drop unnecessary columns
    all_titles = all_titles.drop(
        columns=[
            "age_certification",
            "seasons",
            "imdb_id",
        ]
    )
    # drop rows with null values for important columns
    sanitized_titles = all_titles.dropna(
        subset=[
            "id",
            "title",
            "release_year",
            "genres",
            "production_countries",
            "imdb_score",
            "imdb_votes",
            "tmdb_score",
            "tmdb_popularity",
        ]
    )
    # Convert the DataFrame to a JSON object. ('orient="records"' returns a list of objects)
    titles_list = sanitized_titles.head(10).to_json(orient="records")
    titles_list
  `);

  let recommendations = await self.pyodide.runPythonAsync(`
    # Create recommendation list for Shows and Movies
    # 1. Copy the sanitized_titles to add new column to it
    recommended_titles = sanitized_titles.copy()

    # 2. Add new column to the sanitized_titles
    recommended_titles["recommendation_score"] = (
        sanitized_titles["imdb_votes"] * 0.3 +
        sanitized_titles["imdb_score"] * 0.3 +
        sanitized_titles["tmdb_score"] * 0.2 +
        sanitized_titles["tmdb_popularity"] * 0.2
    )
    # 3. Create Recommended movies list
    recommended_movies = recommended_titles.loc[recommended_titles["type"] == "MOVIE"].sort_values(
        by="recommendation_score", ascending=False
    ).head(5).to_json(orient="records")
    # 4. Create Recommended shows list
    recommended_shows = recommended_titles.loc[recommended_titles["type"] == "SHOW"].sort_values(
        by="recommendation_score", ascending=False
    ).head(5).to_json(orient="records")
    recommendations = {
        "movies": recommended_movies,
        "shows": recommended_shows
    }
    recommendations
  `);

  let facts = await self.pyodide.runPythonAsync(`
    # Create facts list for Movies and Shows
    facts_movies = sanitized_titles.loc[sanitized_titles["type"] == "MOVIE"].groupby("release_year").count()["id"].sort_values(ascending=False).head(1).to_json(orient="table")
    facts_shows = sanitized_titles.loc[sanitized_titles["type"] == "SHOW"].groupby("release_year").count()["id"].sort_values(ascending=False).head(1).to_json(orient="table")
    facts = {
        "movies": facts_movies,
        "shows": facts_shows
    }
    facts
  `);

  self.postMessage({
    titles: titlesList,
    recommendedMovies: recommendations.toJs({
      dict_converter: Object.fromEntries,
    }).movies,
    recommendedShows: recommendations.toJs({
      dict_converter: Object.fromEntries,
    }).shows,
    factsMovies: facts.toJs({ dict_converter: Object.fromEntries }).movies,
    factsShows: facts.toJs({ dict_converter: Object.fromEntries }).shows,
  });
};

在这里,像我们之前做的那样分析完 Netflix 数据后,我们使用postMessage 将结果发布到主线程。

接下来,在const worker = new Worker("./worker.js"); 之后的index.html文件中,添加以下代码。

worker.postMessage("Running Pyodide");
worker.onmessage = function (event) {
  event.data.titles !== undefined ? appComponent.state.titles = event.data.titles : [];
  event.data.recommendedMovies !== undefined ? appComponent.state.recommendedMovies = event.data.recommendedMovies : [];
  event.data.recommendedShows !== undefined ? appComponent.state.recommendedShows = event.data.recommendedShows : [];
  event.data.factsMovies !== undefined ? appComponent.state.factsMovies = event.data.factsMovies : [];
  event.data.factsShows !== undefined ? appComponent.state.factsShows = event.data.factsShows : [];
  appComponent.render()
}

停止并重启Python HTTP服务器。刷新浏览器。

你应该看到和之前一样的结果,但是Pyodide的执行和Python代码被卸载到了一个单独的线程。

总结

在本教程中,我们介绍了如何使用Pandas对我们的Netflix标题CSV数据进行数据处理,以创建电影和节目的推荐分数和列表。我们还做了一些数据分析,找出大部分电影和节目是哪一年制作的。

我们还通过将Pyodide和Python代码的执行转移到一个网络工作者来提高我们的应用程序的性能。

你可以在这里找到本教程的源代码。

在下一篇教程中,我们将为我们的应用程序添加更多的SPA功能,如删除和编辑电影和节目。我们还将添加一个持久化数据层,这样远程数据只需被获取一次。

--

Python单页应用程序系列。

  1. 第一部分。学习Pyodide的基础知识并创建基础应用程序
  2. 第二部分(本教程!)。用Pandas分析和处理数据,并使用网络工作者来加快应用程序的速度
  3. 第三部分(即将推出)。创建一个Python包,添加额外的功能,并添加一个持久的数据层