├── output ├── csv │ └── .gitignore ├── json │ └── .gitignore └── pickles │ └── .gitignore ├── .gitattributes ├── .images ├── code.png ├── db_data.png ├── db_demos.png ├── db_columns.png └── county_rates.png ├── .dockerignore ├── .gitignore ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── lib ├── snake_case.py ├── c19us_combined.py ├── update.py ├── dump_csv_and_json.py ├── c19us_nyt.py ├── c19us_jhu.py ├── upload_to_firestore.py ├── c19all.py ├── county_charts.py └── notebooks │ └── all.ipynb ├── Dockerfile ├── .devcontainer └── devcontainer.json ├── LICENSE.md ├── requirements.txt └── README.md /output/csv/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /output/json/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ipynb linguist-detectable=false 2 | 3 | -------------------------------------------------------------------------------- /output/pickles/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /.images/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willhaslett/covid-19-growth/HEAD/.images/code.png -------------------------------------------------------------------------------- /.images/db_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willhaslett/covid-19-growth/HEAD/.images/db_data.png -------------------------------------------------------------------------------- /.images/db_demos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willhaslett/covid-19-growth/HEAD/.images/db_demos.png -------------------------------------------------------------------------------- /.images/db_columns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willhaslett/covid-19-growth/HEAD/.images/db_columns.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .dockerignore 3 | .byebug_history 4 | log/* 5 | tmp/* 6 | .gitignore 7 | *.swp 8 | *.pkl -------------------------------------------------------------------------------- /.images/county_rates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willhaslett/covid-19-growth/HEAD/.images/county_rates.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .vscode/ 3 | *.pyc 4 | *.swp 5 | lib/__pycache__/ 6 | .google_service_account_key.json 7 | output/charts/* 8 | 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /lib/snake_case.py: -------------------------------------------------------------------------------- 1 | def convert(string): 2 | n = len(string) 3 | string = list(string) 4 | for i in range(n): 5 | if (string[i] == ' '): 6 | string[i] = '_' 7 | else: 8 | string[i] = string[i].lower() 9 | string = "".join(string) 10 | return string 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-buster 2 | RUN apt update -qq \ 3 | && apt install -y --no-install-recommends \ 4 | python-autopep8 \ 5 | libopenblas-dev \ 6 | liblapack-dev \ 7 | texlive-full \ 8 | gfortran 9 | RUN pip install --upgrade pip 10 | RUN mkdir /app 11 | WORKDIR /app 12 | COPY . /app 13 | RUN pip install -r requirements.txt 14 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Local Dockerfile", 3 | "context": "..", 4 | "dockerFile": "../Dockerfile", 5 | "settings": { 6 | "terminal.integrated.shell.linux": null 7 | }, 8 | "extensions": [ 9 | "ms-python.python", 10 | "ryuta46.multi-command", 11 | "himanoa.python-autopep8", 12 | "randomfractalsinc.vscode-data-preview", 13 | "yzhang.markdown-all-in-one" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /lib/c19us_combined.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import pickle 4 | from c19us_jhu import df_us as df_jhu 5 | from c19us_nyt import df_us as df_nyt 6 | 7 | ''' Merged Johns Hopkins and New York Times US county-level data. ''' 8 | 9 | df_jhu = df_jhu[['cases', 'deaths', 'recovered', 'active']] 10 | df_us = df_nyt.join(df_jhu, lsuffix='_nyt', rsuffix='_jhu') 11 | 12 | pickle_file = open('output/pickles/df_us_combined.p', 'wb') 13 | pickle.dump(df_us, pickle_file) 14 | print('Updated pickle file df_us_combined.p with Johns Hopkins and New York Times data') 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. When I '...' 16 | 2. The '....' 17 | 3. Doesn't '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /lib/update.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import pickle 4 | import c19us_nyt 5 | 6 | # For options, see https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_json.html 7 | JSON_ORIENT = 'table' 8 | 9 | # For not-minified JSON, set to > 0 10 | JSON_INDENT = 0 11 | 12 | DATAFRAMES = { 13 | 'df_us_nyt': c19us_nyt.df_us.reset_index(), 14 | } 15 | 16 | for filename in DATAFRAMES: 17 | DATAFRAMES[filename].to_csv(f'output/csv/{filename}.csv', index=False) 18 | DATAFRAMES[filename].to_json(f'output/json/{filename}.json', orient=JSON_ORIENT, indent=JSON_INDENT) 19 | 20 | print('Updated CSV files with New York Times data') 21 | print('Updated JSON files with New York Times data') 22 | -------------------------------------------------------------------------------- /lib/dump_csv_and_json.py: -------------------------------------------------------------------------------- 1 | import c19all 2 | import c19us_jhu 3 | import c19us_nyt 4 | import c19us_combined 5 | import pandas as pd 6 | 7 | """ Generates CSV and JSON files for all available dataframes """ 8 | 9 | # For options, see https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_json.html 10 | JSON_ORIENT = 'table' 11 | 12 | # For not-minified JSON, set to > 0 13 | JSON_INDENT = 0 14 | 15 | DATAFRAMES = { 16 | 'df_all_cases': c19all.df_all['cases'], 17 | 'df_all_deaths': c19all.df_all['deaths'], 18 | 'df_us_jhu': c19us_jhu.df_us.reset_index(), 19 | 'df_us_nyt': c19us_nyt.df_us.reset_index(), 20 | 'df_us_combined': c19us_combined.df_us.reset_index(), 21 | } 22 | 23 | for filename in DATAFRAMES: 24 | DATAFRAMES[filename].to_csv(f'output/csv/{filename}.csv', index=False) 25 | DATAFRAMES[filename].to_json(f'output/json/{filename}.json', orient=JSON_ORIENT, indent=JSON_INDENT) 26 | 27 | print('Updated CSV files') 28 | print('Updated JSON files') 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Will Haslett 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/c19us_nyt.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import urllib 3 | import math 4 | import pickle 5 | import constants 6 | 7 | ''' US county-level data from the New York Times files. ''' 8 | 9 | counties = pd.DataFrame(constants.COUNTIES) 10 | fips = constants.COUNTIES.keys() 11 | county_columns = constants.US_COUNTY_COLUMNS['nyt'] 12 | output_columns = constants.US_OUTPUT_COLUMNS['nyt'] 13 | start_date = constants.START_DATE['nyt'] 14 | 15 | df = pd.read_csv(constants.DATA_URLS['us']['nyt']) 16 | df = df.loc[df.fips.isin(fips)] 17 | df = df.astype({'fips': 'int32'}) 18 | df.date = pd.to_datetime(df.date) 19 | df['start_date'] = pd.to_datetime(start_date) 20 | df['day'] = (df.date - df.start_date).astype('timedelta64[D]') 21 | df.day = df.day.astype('int') 22 | for column in county_columns: 23 | df[column] = df.apply( 24 | lambda row: counties.loc[column, str(row['fips'])], axis=1) 25 | df_us = df[output_columns].set_index(['date', 'fips']) 26 | 27 | try: 28 | get_ipython 29 | except: 30 | pickle_file = open('output/pickles/df_us_nyt.p', 'wb') 31 | pickle.dump(df_us, pickle_file) 32 | print('Updated pickle file df_us_nyt.p with New York Times data') 33 | -------------------------------------------------------------------------------- /lib/c19us_jhu.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from urllib import error, request 3 | import math 4 | import pickle 5 | import constants 6 | 7 | ''' US county-level data from the Johns Hopkins files. ''' 8 | 9 | counties = pd.DataFrame(constants.COUNTIES) 10 | fips = constants.COUNTIES.keys() 11 | county_columns = constants.US_COUNTY_COLUMNS['jhu'] 12 | output_columns = constants.US_OUTPUT_COLUMNS['jhu'] 13 | start_date = constants.START_DATE['jhu'] 14 | 15 | DATE_RANGE = pd.date_range( 16 | start=pd.to_datetime(constants.START_DATE['jhu']), 17 | end=pd.to_datetime('today') 18 | ).tolist() 19 | 20 | def df_from_daily_report(date, url): 21 | df = pd.read_csv( url)[['FIPS', 'Confirmed', 'Deaths', 'Recovered', 'Active']] 22 | df = df.rename(columns=constants.JHU_RENAMED_COLUMNS['daily_reports']) 23 | df = df.loc[df.fips.isin(fips)] 24 | df = df.astype({'fips': 'int32', 'deaths': 'int32', 'recovered': 'int32', 'cases': 'int32'}) 25 | df['date'] = date 26 | df['day'] = (date - pd.to_datetime(start_date)).days 27 | for column in county_columns: 28 | df[column] = df.apply( 29 | lambda row: counties.loc[column, str(row['fips'])], axis=1) 30 | return df[output_columns] 31 | 32 | dfs = [] 33 | for date in DATE_RANGE: 34 | url = constants.DATA_URLS['us']['jhu'].replace( 35 | '##-##-####', date.strftime('%m-%d-%Y')) 36 | try: 37 | response = request.urlopen(url) 38 | except error.HTTPError: 39 | break 40 | else: 41 | dfs.append(df_from_daily_report(date, url)) 42 | 43 | df_us = pd.concat(dfs).set_index(['date', 'fips']) 44 | 45 | try: 46 | get_ipython 47 | except: 48 | pickle_file = open('output/pickles/df_us_jhu.p', 'wb') 49 | pickle.dump(df_us, pickle_file) 50 | print('Updated pickle file df_us_jhu.p with Johns Hopkins data') -------------------------------------------------------------------------------- /lib/upload_to_firestore.py: -------------------------------------------------------------------------------- 1 | import firebase_admin 2 | from firebase_admin import credentials 3 | from firebase_admin import firestore 4 | import pandas as pd 5 | from c19us_combined import df_us 6 | 7 | # Be aware of Firestore pricing: https://firebase.google.com/docs/firestore/pricing 8 | # TODO: 9 | # Async 10 | # Incremental daily uploads 11 | 12 | # Create the database client 13 | cred = credentials.Certificate('.google_service_account_key.json') 14 | firebase_admin.initialize_app(cred, { 15 | 'projectId': 'covidlocal', 16 | }) 17 | db = firestore.client() 18 | 19 | # Turn date and fips into columns 20 | df_us = df_us.reset_index() 21 | 22 | # Create a Firestore document that holds the column names, to accompany values documents 23 | df_us['date_string'] = df_us.date.apply(lambda date: date.strftime('%Y-%m-%d')) 24 | df_us = df_us.drop(columns='date') 25 | columns = {} 26 | for i in range(0, len(df_us.columns)): 27 | columns[str(i)] = df_us.columns[i] 28 | db.collection(u'us-columns').document(u'us-combined').set(columns) 29 | 30 | 31 | # Break up df_us into json strings for each date 32 | date_list = df_us.date_string.unique().tolist() 33 | json_by_date = {} 34 | for date in date_list: 35 | df = df_us[df_us['date_string'] == date] 36 | date_json = df.to_json(orient='values') 37 | json_by_date[date] = date_json 38 | 39 | # Upload JSON string for each date as a document 40 | for date_string, json_string in json_by_date.items(): 41 | db.collection(u'us-data').document(date_string).set({'json': json_string}) 42 | 43 | 44 | def delete_collection(coll_ref, batch_size=100): 45 | docs = coll_ref.limit(batch_size).stream() 46 | deleted = 0 47 | for doc in docs: 48 | print(u'Deleting doc {}'.format(doc.id)) 49 | doc.reference.delete() 50 | deleted = deleted + 1 51 | if deleted >= batch_size: 52 | return delete_collection(coll_ref, batch_size) 53 | 54 | # Delete a collection. This does not work if there are sub-collections present 55 | # https://firebase.google.com/docs/firestore/solutions/delete-collections 56 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appnope==0.1.0 2 | asteval==0.9.18 3 | astroid==2.3.3 4 | attrs==19.3.0 5 | autopep8==1.5.4 6 | backcall==0.1.0 7 | bleach==3.3.0 8 | CacheControl==0.12.6 9 | cachetools==4.0.0 10 | certifi==2019.11.28 11 | chardet==3.0.4 12 | cycler==0.10.0 13 | data==0.4 14 | decorator==4.4.2 15 | defusedxml==0.6.0 16 | entrypoints==0.3 17 | firebase-admin==4.0.0 18 | funcsigs==1.0.2 19 | future==0.18.2 20 | google-api-core==1.16.0 21 | google-api-python-client==1.8.0 22 | google-auth==1.11.3 23 | google-auth-httplib2==0.0.3 24 | google-cloud-core==1.3.0 25 | google-cloud-firestore==1.6.2 26 | google-cloud-storage==1.26.0 27 | google-resumable-media==0.5.0 28 | googleapis-common-protos==1.51.0 29 | grpcio==1.27.2 30 | httplib2==0.18.0 31 | idna==2.9 32 | importlib-metadata==1.5.0 33 | ipykernel==5.1.4 34 | ipython==7.13.0 35 | ipython-genutils==0.2.0 36 | ipywidgets==7.5.1 37 | isort==4.3.21 38 | jedi==0.16.0 39 | Jinja2==2.11.1 40 | json5==0.9.5 41 | jsonschema==3.2.0 42 | jupyter==1.0.0 43 | jupyter-client==6.0.0 44 | jupyter-console==6.1.0 45 | jupyter-core==4.6.3 46 | jupyterlab==2.2.9 47 | jupyterlab-server==1.2.0 48 | kiwisolver==1.1.0 49 | latex==0.7.0 50 | lazy-object-proxy==1.4.3 51 | lmfit==1.0.0 52 | MarkupSafe==1.1.1 53 | matplotlib==3.2.0 54 | mccabe==0.6.1 55 | mistune==0.8.4 56 | msgpack==1.0.0 57 | nbconvert==5.6.1 58 | nbformat==5.0.4 59 | notebook==6.1.5 60 | numpy==1.19.2 61 | pandas==1.1.3 62 | pandocfilters==1.4.2 63 | parso==0.6.2 64 | pexpect==4.8.0 65 | pickleshare==0.7.5 66 | prometheus-client==0.7.1 67 | prompt-toolkit==3.0.4 68 | protobuf==3.11.3 69 | ptyprocess==0.6.0 70 | pyasn1==0.4.8 71 | pyasn1-modules==0.2.8 72 | pycodestyle==2.6.0 73 | Pygments==2.6.1 74 | pylint==2.4.4 75 | pyparsing==2.4.6 76 | pyrsistent==0.15.7 77 | python-dateutil==2.8.1 78 | pytz==2019.3 79 | pyzmq==19.0.0 80 | qtconsole==4.7.1 81 | QtPy==1.9.0 82 | requests==2.23.0 83 | rsa==4.0 84 | scipy==1.5.2 85 | Send2Trash==1.5.0 86 | shutilwhich==1.1.0 87 | six==1.14.0 88 | tempdir==0.7.1 89 | terminado==0.8.3 90 | testpath==0.4.4 91 | toml==0.10.1 92 | tornado==6.0.4 93 | traitlets==4.3.3 94 | typed-ast==1.4.1 95 | uncertainties==3.1.2 96 | uritemplate==3.0.1 97 | urllib3==1.25.8 98 | wcwidth==0.1.8 99 | webencodings==0.5.1 100 | widgetsnbextension==3.5.1 101 | wrapt==1.11.2 102 | zipp==3.1.0 103 | -------------------------------------------------------------------------------- /lib/c19all.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from operator import itemgetter 3 | import pickle 4 | import constants 5 | 6 | 7 | """ Exposes df_all, a dictionary with dataframes holding all global time series data 8 | df_all = { 9 | 'cases': , 10 | 'deaths': 11 | } 12 | 13 | Dataframe functions 14 | `filter(df, column, vlaue)` Generic filter 15 | `for_country(df, country)` Filter by country 16 | `for_province_state(df, province_state)` Filter by province_state 17 | `sum_by_date(df)` Group by date and sum case counts 18 | `date_to_day(date)` Convert a date to the number of days since the date of the first records 19 | `day_to_date(day)` Convert a number of days since the first records to a date 20 | """ 21 | 22 | renamed_columns = constants.JHU_RENAMED_COLUMNS['time_series'] 23 | 24 | # Perform ETL on a Johns Hopkins COVID-19 time series file, Returning a dataframe 25 | def df_from_csv(file_name): 26 | df = pd.read_csv(file_name) 27 | df = df.rename(columns=renamed_columns) 28 | date_cols = df.filter(regex=('^\d+/\d+/\d+$')).columns.array 29 | df = pd.melt(df, id_vars=['province_state', 'country', 'lat', 30 | 'long'], value_vars=date_cols, var_name='date', value_name='cases') 31 | df.date = pd.to_datetime(df.date, format='%m/%d/%y') 32 | df['day'] = (df.date - pd.to_datetime(df.date.iloc[0])).astype('timedelta64[D]') 33 | df.day = df.day.apply(lambda day: int(round(day))) 34 | return df[['date', 'day', 'cases', 'province_state', 'country', 'lat', 'long']] 35 | 36 | # General purpose filter 37 | def filter(df, column, value): 38 | return df[df[column] == value].reset_index() 39 | 40 | # Filter on country 41 | def for_country(df, country): 42 | return filter(df, 'country', country) 43 | 44 | # Filter on province_state. us.py has its own function for this 45 | def for_province_state(df, province_state): 46 | return filter(df, 'province_state', province_state) 47 | 48 | # Return input with all rows collapsed by date and cases summed 49 | def sum_by_date(df): 50 | return df.groupby('date').sum().reset_index() 51 | 52 | # Convert a date to the number of days since the date of the first records 53 | def date_to_day(date): 54 | return (date - pd.to_datetime('2020-01-21')).days 55 | 56 | # Convert a number of days since the first records to a date 57 | def day_to_date(day): 58 | pd.to_datetime('2020-03-21') + pd.DateOffset(days=day) 59 | 60 | _df_cases = df_from_csv(constants.DATA_URLS['global']['cases']) 61 | _df_deaths = df_from_csv(constants.DATA_URLS['global']['deaths']).rename(columns={'cases': 'deaths'}) 62 | 63 | # Dictionary containing dataframes for all global data 64 | df_all = { 65 | 'cases': _df_cases, 66 | 'deaths': _df_deaths 67 | } 68 | 69 | try: 70 | get_ipython 71 | except: 72 | pickle_file = open('output/pickles/df_all.p', 'wb') 73 | pickle.dump(df_all, pickle_file) 74 | print('Updated pickle file df_all.p with global data') 75 | -------------------------------------------------------------------------------- /lib/county_charts.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import matplotlib as mpl 4 | import matplotlib.pyplot as plt 5 | import matplotlib.dates as dt 6 | import matplotlib.ticker as ticker 7 | import datetime 8 | import pickle 9 | import copy 10 | import snake_case 11 | 12 | YAXPARAMS = { 13 | 'cases': { 14 | 'total': { 15 | 'ymax': 90, 16 | 'yinterval':10 17 | }, 18 | 'adj': { 19 | 'ymax': 100, 20 | 'yinterval': 10 21 | } 22 | }, 23 | 'deaths': { 24 | 'total': { 25 | 'ymax': 5, 26 | 'yinterval': 1 27 | }, 28 | 'adj': { 29 | 'ymax': 5, 30 | 'yinterval': 1 31 | } 32 | }, 33 | } 34 | 35 | YINTERVAL_TOTAL = 5 36 | YINTERVAL_ADJ = 10 37 | 38 | SOURCE_LABELS = { 39 | 'nyt': 'New York Times', 40 | 'jhu': 'Johns Hopkins University' 41 | } 42 | 43 | STATE_COLORS = { 44 | 'Vermont': '#1f77b4', 45 | 'New Hampshire': '#871f78', 46 | } 47 | 48 | df_start = pickle.load( 49 | open('output/pickles/df_us_nyt.p', 'rb')).reset_index() 50 | 51 | # If you pass in a population, the output will be per 1,000 people 52 | # If you pass in an output filename, the plots will be written to ./images and not rendered to the screen 53 | def county_plot(county, state, metrics=['cases', 'deaths'], source='nyt', total_population=None): 54 | df = copy.deepcopy(df_start) 55 | start_date = pd.to_datetime('2020-03-01') 56 | location = { 57 | 'type': 'county', 58 | 'value': [county, state] 59 | } 60 | for metric in metrics: 61 | for population in [False, total_population]: 62 | count_of = f'{metric}' 63 | county = location['value'][0] 64 | state = location['value'][1] 65 | color = STATE_COLORS[state] 66 | df = df[df.county == county] 67 | df = df[df.state == state] 68 | df = df[df.date >= start_date] 69 | if population: 70 | df[count_of] = df[count_of].apply(lambda x: (x / population) * 100000) 71 | df['count_of_diff'] = df[count_of].diff() 72 | df['count_of_diff_7_day_mean'] = df.count_of_diff.rolling(7).mean() 73 | df = df.iloc[1:] 74 | fig = plt.figure(figsize=(7, 3)) 75 | ax = fig.add_subplot(111) 76 | ax.bar('date', 'count_of_diff', data=df, color=color, alpha=0.35) 77 | ax.plot('date', 'count_of_diff_7_day_mean', color=color, data=df) 78 | ax.xaxis.set_major_locator(dt.MonthLocator()) 79 | ax.xaxis.set_major_formatter(dt.DateFormatter('%b')) 80 | ax.set_ylim(ymin=0) 81 | yaxparams = YAXPARAMS[metric]['adj' if population else 'total'] 82 | ymax = yaxparams['ymax'] 83 | yinterval = yaxparams['yinterval'] 84 | # ax.set_ylim(ymax=yaxparams['ymax']) 85 | ax.yaxis.set_ticks(np.arange(0, ymax + yinterval, yinterval)) 86 | ax.yaxis.set_major_formatter(ticker.FormatStrFormatter('%0.0f')) 87 | ax.tick_params(axis='y', colors=color) 88 | ax.tick_params(axis='x', colors=color) 89 | ax.spines['top'].set_visible(False) 90 | ax.spines['right'].set_visible(False) 91 | ax.spines['left'].set_visible(False) 92 | ax.grid(axis='x') 93 | plt.style.use('seaborn-whitegrid') 94 | plt.text(df.date.iloc[-1] + datetime.timedelta(days=3), df.count_of_diff_7_day_mean.iloc[-1], 95 | "7-day\navg.", color=color, style='italic') 96 | filename = snake_case.convert(f'{county} {state} {metric}{" adjusted" if population else ""}.svg') 97 | plt.savefig(f'output/charts/{filename}') 98 | 99 | 100 | county_dicts = [ 101 | {'county': 'Orange', 'state': 'Vermont', 'total_population': 28892}, 102 | {'county': 'Orange', 'state': 'Vermont', 'total_population': 28892}, 103 | {'county': 'Windsor', 'state': 'Vermont', 'total_population': 55062}, 104 | {'county': 'Grafton', 'state': 'New Hampshire', 'total_population': 89886}, 105 | {'county': 'Sullivan', 'state': 'New Hampshire', 'total_population': 43146}, 106 | ] 107 | 108 | for county_dict in county_dicts: 109 | county_plot(**county_dict) 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # covid-19-growth 2 | 3 | The [New York Times](https://github.com/nytimes/covid-19-data) 4 | and the [Johns Hopkins University Center for Systems Science and Engineering](https://github.com/CSSEGISandData/COVID-19) 5 | are providing daily Covid-19 case and death data, and they are performing ad hoc revisions to existing data. This pipeline: 6 | - Imports the NYT and JHU data 7 | - Performs type conversions where needed 8 | - Applies consistent snake_case naming for all attributes, across sources, preserving fidelity to original meanings 9 | - For the US, facilitates aggregation at the region, sub-region, state, and county levels, and adds county-level population and lat/long 10 | - Outputs the resulting data structures as a set of long-format time series 11 | - Includes Jupyter Notebook stubs for working with the transformed data 12 | - Facilitiates uploading the tranformed data to Firebase for mobile/cloud use cases 13 | 14 | 15 | The latest NYT and JHU files are pulled from GitHub at runtime. All data operations are vectorized. All data from 2020-01-21 to the present are imported whenever you run `lib/update.py` to generate new CSV, JSON, or Pickle files. This ensures that all revisions to the JHU or NYT raw files will be included here. 16 | 17 | 18 | - [Installing](#installing) 19 | - [Virtualenv](#virtualenv) 20 | - [VSCode](#vscode) 21 | - [Usage](#usage) 22 | - [What do I get?](#what-do-i-get) 23 | - [CSV, JSON, and Pickle files for the transformed data](#csv-and-json) 24 | - [Global Data](#global-data) 25 | - [US Data](#us-data) 26 | - [Jupyter Notebooks](#jupyter-notebooks) 27 | - [Firebase](#firebase) 28 | - [License](#license) 29 | - [Acknowledgments](#acknowledgments) 30 | 31 | 32 | ## Installing 33 | ### Virtualenv 34 | Copy/paste 35 | ``` 36 | git clone https://github.com/willhaslett/covid-19-growth.git 37 | cd covid-19-growth 38 | virtualenv venv 39 | source venv/bin/activate 40 | pip install -q -r requirements.txt 41 | 42 | ``` 43 | Verify installation 44 | ``` 45 | $ python lib/update.py 46 | Updated pickle file df_all.p with global data 47 | Updated pickle file df_us_jhu.p with Johns Hopkins data 48 | Updated pickle file df_us_nyt.p with New York Times data 49 | Updated pickle file df_us_combined.p with Johns Hopkins and New York Times data 50 | Updated CSV files 51 | Updated JSON files 52 | Output Pickle, CSV and JSON files are up-to-date. For further work in Python, import the Pickles! 53 | $ 54 | ``` 55 | 56 | ### VSCode/Docker 57 | 58 | 59 | If you'd like to use Docker, or if you have Python environment reasons to use Docker, VSCode makes it easy to get up and running. 60 | 61 | 62 | - Have the [VSCode extension for Remote Development](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack) installed. Here, 'remote' means in a local Docker container (Debian). 63 | - In VSCode, [Open the project folder in a container](https://code.visualstudio.com/docs/remote/containers#_quick-start-open-an-existing-folder-in-a-container) 64 | - Verify the installation as above. 65 | 66 | ![](.images/code.png) 67 | 68 | 69 | ## Usage 70 | 71 | ### What do I get? 72 | Two sets of output data are constructed at runtime, one for all global data and one for all US data. 73 | The US data are parsed and demographic data are added. 74 | The NYT and JHU US data are available separately and as a combined time series. 75 | 76 | The three output formats, Pandas, CSV and JSON, all contain the same data, with the dataframes and CSV files 77 | having the same tabular format, and the JSON files structured by the 78 | [pandas.DataFrame.to_json](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_json.html) function. 79 | 80 | - Global data (JHU) 81 | - df_all_cases 82 | - df_all_deaths 83 | - US data 84 | - df_us_jhu 85 | - df_us_nyt 86 | - df_us_combined 87 | 88 | ### Global Data 89 | `c19all.py` 90 | * **`df_all`** A dictionary containing dataframes with all global data for cases, deaths, and recoveries. `province_state` has mixed types, as it does upstream. 91 | ``` 92 | print(df_all['cases']) 93 | 94 | date day cases province_state country lat long 95 | 0 2020-01-22 0 0 NaN Afghanistan 33.000000 65.000000 96 | 1 2020-01-22 0 0 NaN Albania 41.153300 20.168300 97 | 2 2020-01-22 0 0 NaN Algeria 28.033900 1.659600 98 | 3 2020-01-22 0 0 NaN Andorra 42.506300 1.521800 99 | 4 2020-01-22 0 0 NaN Angola -11.202700 17.873900 100 | ... ... ... ... ... ... ... ... 101 | 16429 2020-03-27 65 2 NaN Saint Kitts and Nevis 17.357822 -62.782998 102 | 16430 2020-03-27 65 1 Northwest Territories Canada 64.825500 -124.845700 103 | 16431 2020-03-27 65 3 Yukon Canada 64.282300 -135.000000 104 | 16432 2020-03-27 65 86 NaN Kosovo 42.602636 20.902977 105 | 16433 2020-03-27 65 8 NaN Burma 21.916200 95.956000 106 | 107 | [16434 rows x 7 columns] 108 | ``` 109 | 110 | * **Functions** 111 | - `filter(df, column, vlaue)` Generic filter 112 | - `for_country(df, country)` Filter by country 113 | - `for_province_state(df, province_state)` Filter by province_state 114 | - `sum_by_date(df)` Group by date and sum case counts 115 | 116 | ### US Data 117 | The three output US data structures all have the same basic shape. 118 | Note however that whereas the NYT time series starts on 2020-01-21, the JHU time series 119 | starts on 2020-03-22, the date when JHU changed the format of their US data. 120 | `date` and `fips` are used as a multindex in Pandas, and these are added as columns 121 | in the CSV and JSON files. 122 | 123 | `c19us_jhu.df_us` and `c19us_nyt.df_us` are combined in `c19us_combined` as shown below. 124 | Here, the suffixes `_nyt`and `_jhu` are added to the case and death data. 125 | ``` 126 | >>> from c19us_combined import df_us 127 | >>> print(df_us) 128 | day county state sub_region region lat long cases_nyt deaths_nyt cases_jhu deaths_jhu recovered active 129 | date fips 130 | 2020-01-21 53061 0 Snohomish Washington pacific west 48.04615983 -121.7170703 1 0 NaN NaN NaN NaN 131 | 2020-01-22 53061 1 Snohomish Washington pacific west 48.04615983 -121.7170703 1 0 NaN NaN NaN NaN 132 | 2020-01-23 53061 2 Snohomish Washington pacific west 48.04615983 -121.7170703 1 0 NaN NaN NaN NaN 133 | 2020-01-24 17031 3 Cook Illinois east_north_central midwest 41.84144849 -87.81658794 1 0 NaN NaN NaN NaN 134 | 53061 3 Snohomish Washington pacific west 48.04615983 -121.7170703 1 0 NaN NaN NaN NaN 135 | ... . ... ... ... ... ... ... ... ... ... ... ... ... 136 | 2020-03-26 56025 65 Natrona Wyoming mountain west 42.96180148 -106.797885 6 0 6.0 0.0 0.0 0.0 137 | 56029 65 Park Wyoming mountain west 44.52157546 -109.5852825 1 0 1.0 0.0 0.0 0.0 138 | 56033 65 Sheridan Wyoming mountain west 44.79048913 -106.8862389 4 0 4.0 0.0 0.0 0.0 139 | 56037 65 Sweetwater Wyoming mountain west 41.65943896 -108.8827882 1 0 1.0 0.0 0.0 0.0 140 | 56039 65 Teton Wyoming mountain west 43.93522482 -110.5890801 8 0 7.0 0.0 0.0 0.0 141 | 142 | [13832 rows x 13 columns] 143 | >>> 144 | ``` 145 | 146 | ## CSV and JSON 147 | `dump_csv_and_json.py` gets executed when you run `lib/update.py`. 148 | It creates CSV and JSON files for the five Pandas dataframes and puts them in `output/csv` and `output/json`. 149 | 150 | - **CSV:** 151 | Comma-delimited files for each dataframe. The formats mirror the dataframes as described above. 152 | 153 | - **JSON:** 154 | JavaScript Object Notation files for each dataframe. Files are constructed using the `orient='table'` argument for 155 | [pandas.DataFrame.to_json](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_json.html). 156 | Choose a different structure for the JSON files by setting `JSON_ORIENT` in `lib/dump_csv_and_json.py`. JSON files are minified by default. For not-minified JSON, set `JSON_INDENT` to > 0. 157 | 158 | ## Jupyter Notebooks 159 | 160 | `all.ipynb` and `us.ipynb` contain example starting points for work with global or US data. 161 | 162 | ## Firebase 163 | ### Prerequisites 164 | - [Create your Firebase project](https://firebase.google.com/) and add a Firestore database. 165 | - Create and download a private key JSON file for your project. (Project settings > Service accounts) 166 | - Rename the downladed file to `.google_service_account_key.json` and put it in the project root. This file will be ignored by Git. 167 | 168 | ### Usage 169 | `python lib/upload_to_firestore.py` 170 | 171 | This script uploads the combined US data structure to Firestore using the following scheme: 172 | 1. A schema document that defines the column names for the associated data documents 173 | ![](.images/db_columns.png) 174 | 1. A collection of data documents, split by date, with all data for a date stored in a single JSON string 175 | ![](.images/db_data.png) 176 | 3. A document containing the available additional data for all counties in the dataset, keyed by fips code 177 | ![](.images/db_demos.png) 178 | 179 | ## License 180 | 181 | This project is licensed under the MIT License. See the [LICENSE.md](LICENSE.md) file for details 182 | 183 | ## Acknowledgments 184 | 185 | The New York Times and the Johns Hopkins University Center for Systems Science and Engineering are doing a great public service by sharing these data. 186 | 187 | -------------------------------------------------------------------------------- /lib/notebooks/all.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "data": { 10 | "text/plain": "" 11 | }, 12 | "execution_count": 2, 13 | "metadata": {}, 14 | "output_type": "execute_result" 15 | }, 16 | { 17 | "data": { 18 | "image/png": "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\n", 19 | "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", 20 | "text/plain": "
" 21 | }, 22 | "metadata": { 23 | "transient": {} 24 | }, 25 | "output_type": "display_data" 26 | } 27 | ], 28 | "source": [ 29 | "import pandas as pd\n", 30 | "import matplotlib as mpl\n", 31 | "import matplotlib.pyplot as plt\n", 32 | "import pickle\n", 33 | "from lmfit import models\n", 34 | "import sys\n", 35 | "import os\n", 36 | "sys.path.insert(0, os.path.abspath('../'))\n", 37 | "import c19all\n", 38 | "\n", 39 | "df = pickle.load(open( '../../output/pickles/df_all.p', 'rb'))\n", 40 | "\n", 41 | "''' Define the analytic dataset\n", 42 | " count_of: required, `cases` or `deaths`\n", 43 | " start_date: required, >= 2020-01-02\n", 44 | " country: optional, comment out the declaration for global data\n", 45 | "'''\n", 46 | "count_of = 'cases'\n", 47 | "start_date = pd.to_datetime('2020-01-21')\n", 48 | "country = 'Japan'\n", 49 | "\n", 50 | "# See https://lmfit.github.io/lmfit-py/builtin_models.html for model options\n", 51 | "df = df[count_of]\n", 52 | "if 'country' in locals():\n", 53 | " df = c19all.for_country(df, country)\n", 54 | " ylabel = f'{count_of.capitalize()} in {country}' if country else f'Global {count_of}'\n", 55 | "else:\n", 56 | " ylabel = f'Global {count_of}'\n", 57 | "df = df[df.date >= start_date]\n", 58 | "df.day = df.day.apply(lambda day: (day - (c19all.date_to_day(start_date ) - 1)))\n", 59 | "df = df.groupby('day').sum().reset_index()\n", 60 | "model = models.PowerLawModel()\n", 61 | "params = model.make_params()\n", 62 | "result = model.fit(df[count_of], params, x=df.day.to_list())\n", 63 | "plt.style.use('ggplot')\n", 64 | "xlabel = f'Days since {start_date.strftime(\"%Y-%m-%d\")}'\n", 65 | "result.plot_fit(xlabel=xlabel, ylabel=ylabel, datafmt='og', fitfmt='r')" 66 | ] 67 | } 68 | ], 69 | "metadata": { 70 | "file_extension": ".py", 71 | "kernelspec": { 72 | "display_name": "Python 3.8.2 64-bit", 73 | "name": "python38264bit638d3b9becec457392c33150d78edef7" 74 | }, 75 | "language_info": { 76 | "codemirror_mode": { 77 | "name": "ipython", 78 | "version": 2 79 | }, 80 | "name": "python", 81 | "version": "3.8.2-final" 82 | }, 83 | "mimetype": "text/x-python", 84 | "name": "python", 85 | "npconvert_exporter": "python", 86 | "orig_nbformat": 2, 87 | "pygments_lexer": "ipython2", 88 | "version": 2 89 | }, 90 | "nbformat": 4, 91 | "nbformat_minor": 2 92 | } --------------------------------------------------------------------------------