COVID仪表板升级的代码片段示例

106 阅读5分钟

COVID仪表盘的升级

我最近升级了我之前建立的COVID Rails仪表盘。在这篇文章中,我描述了升级的动机和细节,同时分享了代码片段。

早在2020年,我就用Rails 6.0.2和Ruby 2.5.1编写了一个COVID仪表盘。给定一个日期范围,它显示确认、死亡和恢复案例数量的累积以及增量数据的线图。它的数据来源是:

  1. 约翰霍普金斯大学系统科学与工程中心(CSSE)的COVID-19数据存储库。这个GitHub资源库每天提供一个CSV文件,其中包含国家层面的累积数据。

  2. 这个网站曾经提供印度各州的统计数据,但不幸的是已经停止了运作。州一级的数据只提供到2021年10月30日,可以通过REST API获得JSON格式的数据。

CSSE每天提供累积数据,而covid19india.org每天提供增量数据。

我最近升级了这个应用程序。在这篇文章中,我描述了升级的动机和细节,并分享了代码片段。

目标

该系统有两个PythonRuby脚本来生成CSV格式的数据集。这些数据被插入Rails仪表盘所访问的数据库中。

这四个生成器脚本是:

  • gdc.py:GDC代表全球每日累积。该脚本将CSV文件作为一个命令行参数,提取每个国家的数据,并打印出来。输出被重定向到datasets/global_daily_cumulative 文件夹中的一个文件。在控制台中运行它的命令是。

Shell

python3 gdc.py ../COVID-19/csse_covid_19_data/csse_covid_19_daily_reports/04-25-2020.csv > datasets/global_daily_cumulative/04-25-2020.csv
  • gdd.py:GDD代表全球每日三角洲。该脚本通过用当天的数字减去前一天的累积数字来计算一天的Delta数字。输出被重定向到datasets/global_daily_delta 文件夹中的一个文件。在控制台中运行它的命令是:

Shell

python3 gdd.py datasets/global_daily_cumulative/04-25-2020.csv > datasets/global_daily_delta/04-25-2020.csv
  • idd.rb:IDD代表印度每日指数。该脚本提取一天的数据,其日期作为命令行参数提供。输出被重定向到datasets/india_daily_delta 文件夹中的一个文件。在控制台中运行它的命令是:

Shell

ruby idd.rb 25-Apr-20 > datasets/india_daily_delta/04-25-2020.csv

注意covid19india.org使用的日期格式是dd-month-yy:

  • idc.rb:IDC代表印度每日累积。该脚本通过将当天的增量(delta)数字与前一天的累积数字相加,计算出一天的累积数据。输出被重定向到datasets/india_daily_cumulative 文件夹中的一个文件。在控制台中运行它的命令是。

Shell

ruby idc.rb 04-25-2020 > datasets/india_daily_cumulative/04-25-2020.csv

daily_all.rb程序读取每个数据集子目录下的所有CSV文件,并制作整理文件内容的聚合数据文件。例如,global_daily_cumulative文件夹中所有文件的内容被整理成global_daily_cumulative.csv。主文件中的每一行都会把数据的日期作为第一个字段。文件名是:

  • global_daily_cumulative.csv
  • global_daily_delta.csv
  • india_daily_cumulative.csv(印度每日累计)。
  • india_daily_delta.csv

生成的每日文件和四个汇总的数据文件都有相同的数据格式:日期、地点、确认、死亡、恢复。

问题是,我不得不为一天的数据运行这四个程序。当然,我可以在一个脚本文件中为一周的数据串联一套命令,然后运行该文件,但仍然需要人工来准备这些命令。

我曾经通过使用psql ,把CSV文件作为一个参数传递给PostgreSQL,把数据插入。一个典型的命令如下(在Windows上)。

PowerShell

"C:\Program Files\PostgreSQL\13\bin\psql.exe" -h localhost -U postgres -d covid19 -c "SET client_encoding TO 'UTF8';" -c "\copy global_daily_cumulative(date, place, confirmed, deaths, recovered) FROM 'E:\Code\Corona2020\covid19\global_daily_cumulative.csv' DELIMITER ',' CSV;"

在Linux上,该命令是:

Shell

psql -d covid19 -c "SET client_encoding TO 'UTF8';" -c "\copy india_daily_delta(date, place, confirmed, deaths, recovered) FROM '/var/www/datasets/covid19/india_daily_delta.csv' DELIMITER ',' CSV;"  

完整的工作流程如下图所示: 有三个手动步骤。我的目标是做端到端的自动化。我的想法是,只做一个git克隆或拉到我的电脑上,然后运行一个Ruby文件。就这样了。同时,我还打算升级仪表盘的技术栈

实施

我选择的语言肯定是Ruby,所以我把两个Python程序也移植到Ruby上。四个生成器脚本变成了Ruby Interactor类,其中的业务逻辑进入了调用方法。 同样地,我将psql命令封装在一个名为InsertCovidDataInDb 的交互器中。它将global_daily_cumulative.csv、global_daily_delta.csv、india_daily_cumulative.csv和india_daily_delta.csv文件中的数据分别插入到global_daily_cumulative、 global_daily_delta、india_daily_delta和india_daily_cumulative表中。

交互器是一个Ruby类,它将包括交互器宝石,并有一个调用方法,你在其中编写业务逻辑。在不创建该类对象的情况下,你调用 "调用 "方法来运行业务逻辑。这种用法类似于Java中的静态方法。文件gdc.rb中有一个交互器类GenerateGdcData ,gdd.rb中有GenerateGddData ,idd.rb中有GenerateIddData ,最后idc.rb中有GenerateIdcData 类。作为一个例子,下面给出的是GenerateGdcData 的代码。

Ruby

require 'interactor'
require 'date'
require 'csv'

$indexes_to_read = {}
$indexes_to_read['format1'] = [1,3,4,5]
$indexes_to_read['format2'] = [3,7,8,9]
   
class GenerateGdcData
    include Interactor
           
    def call
        country_data_hash = {}
        Dir[context.folder + '/*.csv'].each do |file_path|
            file_name = File.basename(file_path, ".*")
           
            # if file_name is before 03-22-2020, file_format = format1 else format2
            file_date     = Date.new(file_name[6..9].to_i, file_name[0..1].to_i, file_name[3..4].to_i)
            file_format   = file_date < Date.new(2020, 03, 22) ? "format1" : "format2"
            file_date_str = file_date.to_s
           
            country_index     = $indexes_to_read[file_format][0]
            confirmed_index   = $indexes_to_read[file_format][1]
            deaths_index      = $indexes_to_read[file_format][2]
            recovered_index   = $indexes_to_read[file_format][3]

            CSV.foreach(file_path, headers: true) do |row|
                country = row[country_index]
                if country == "Mainland China"
                    country = "China"
                elsif country == "Korea, North"
                    country = "South Korea"
                elsif country == "Korea, South"
                    country = "South Korea"
                elsif country == "Gambia, The"
                    country = "Gambia"
                elsif country == "Bahamas, The"
                    country = "Bahamas"
                elsif country == "The Bahamas"
                    country = "Bahamas"
                elsif country == "Gambia, The"
                    country = "Gambia"
                elsif country == "The Gambia"
                    country = "Gambia"
                end

                row[confirmed_index] = row[confirmed_index] ? row[confirmed_index] : 0
                row[deaths_index]    = row[deaths_index]    ? row[deaths_index]    : 0
                row[recovered_index] = row[recovered_index] ? row[recovered_index] : 0
               
                confirmed = row[confirmed_index].to_i
                deaths    = row[deaths_index].to_i
                recovered = row[recovered_index].to_i
               
                if file_format == "format2"
                    row[0] = row[0] ? row[0] : 0
                    row[5] = row[5] ? row[5] : 0.0
                    row[6] = row[6] ? row[6] : 0.0
                end

                if country_data_hash.has_key? [file_date,country]
                    country_data_hash[[file_date_str,country]][0] += confirmed if confirmed
                    country_data_hash[[file_date_str,country]][1] += deaths if deaths
                    country_data_hash[[file_date_str,country]][2] += recovered if recovered
                else
                    country_data_hash[[file_date_str,country]] = [confirmed, deaths, recovered]
                end
            end
        end
        context.gdc = country_data_hash
    end
end

主程序在一个新文件中,generate_covid19_data.rb。它的功能是直接的:调用五个交互器。 tailwind在自动化升级的同时,我还升级了堆栈:即Ruby从2.5.1升级到Ruby 3.1.1;Rails从6.0.2升级到7.0.2.3,加上importmap ;Chartkick从3.4.2升级到4.1.3;最后,PostgreSQL从12到14。

每当我把一个现有的应用程序升级到Rails 7时,我的方法是生成一个示例应用程序,并把不能用的文件复制到现有的应用程序上。具体来说,这些文件是bin、lib和app/javascript文件夹。在那里,我根据我所选择的打包方法做其他的修改。在本例中,我从webpacker 改为importmap ,所以我需要确保bin/importmap.rb文件的存在,以及application.html.erb中的javascript_importmap_tags。

如何运行

克隆CSSE GitHub repo:

Shell

$ git clone https://github.com/CSSEGISandData/COVID-19.git

克隆我的资源库:

Shell

$ git clone https://github.com/mh-github/covid19.git

在你的 PostgreSQL 数据库中创建一个名为 covid19 的数据库。在其中创建四个表。这些命令可以在GitHub上从第92行开始使用

用你的数据库用户名、密码和端口号更新 covid19/rails/dashboard/config/database.yml。在insert_covid_data_in_db.rb文件中也输入同样的值。

转到项目根目录下:

Shell

$ cd covid19

你将需要ruby-3.1.1。如果你使用rvm,安装这个版本并使用:

Shell

$ rvm install 3.1.1
$ rvm use 3.1.1

安装httparty,pg, 和interactor 宝石,如果你的系统中没有这些宝石:

Shell

$ gem install httparty pg interactor

在数据库中插入数据:

Shell

$ ruby generate_covid19_data.rb path/to/COVID-19

转到仪表板文件夹:

Shell

$ cd rails/dashboard

切换到ruby 3.1.1:

Shell

$ rvm use 3.1.1

安装所需的宝石:

Shell

$ bundle install

运行服务器:

Shell

$ rails s

在你的浏览器中访问仪表板http://locahost:3000。这是一张截图,显示澳大利亚的图表。

我放了代码来捕捉数据插入的时间。psql看起来相当快。

考虑到我的基础设施是我的台式电脑,配备i5-4570 CPU @ 3.20GHz处理器,20GB内存,Windows 10 Pro v21H1和WSL2,这些都不坏。我在Windows上运行PostgreSQL,在WSL-Ubuntu上运行我的主要程序。

总结

无论项目大小,总是有机会进行重构和自动化。即使你的程序是一个基本的教程类型,将其技术栈升级到最新的版本也会给你带来实践和学习,你可以将其应用于你的工作应用中