Так что это проект, над которым я работал последние несколько недель. Только начал изучать Python. Начал со сценариев bash и захотел узнать больше. В любом случае код извлекает данные о covid-19 из Интернета, собирает данные так, как мне нужно, отображает их на графиках, которые сохраняются как файлы .png, записывает все данные и графики в файл README.md и отправляет их в github. Поскольку я самоучка, я публикую это для обратной связи, чтобы у меня не развивались вредные привычки и я мог изучить передовой опыт. Любые советы или критика будут оценены. Спасибо.
#!/usr/bin/env python3
import requests
import pandas as pd
import io
import numpy as np
import matplotlib.pyplot as plt
import os
# **** Build data ****
# Fetch data
url="https://raw.githubusercontent.com/nytimes/covid-19-data/master/us.csv"
download = requests.get(url).content
df = pd.read_csv(io.StringIO(download.decode('utf-8')))
# Extract each column individually
date = df['date']
cases = df['cases']
deaths = df['deaths']
# Calculate new cases
total_cases = np.array(cases)
new_cases = np.diff(total_cases)
new_cases = np.insert(new_cases, 0, 1)
# Calculate new deaths
total_deaths = np.array(deaths)
new_deaths = np.diff(total_deaths)
new_deaths = np.insert(new_deaths, 0, 0)
# Create csv for total cases and deaths
df = pd.DataFrame({'date': date, 'total cases': total_cases,
'total deaths': total_deaths})
df.to_csv('data/us_covid-19_total.csv', index=False)
# Create csv for new cases and deaths
df = pd.DataFrame({'date': date, 'new cases': new_cases,
'new deaths': new_deaths})
df.to_csv('data/us_covid-19_new.csv', index=False)
# Create csv for all aggregated data
df = pd.DataFrame({'date': date, 'total cases': total_cases,
'total deaths': total_deaths, 'new cases': new_cases, 'new deaths': new_deaths})
df.to_csv('data/us_covid-19_data.csv', index=False)
# **** Plot data ****
# x axis for all plots
x = np.array(date, dtype="datetime64")
# Plot Total Cases
y = total_cases / 1000000
plt.figure('US Total COVID-19 Cases', figsize=(15, 8))
plt.title('US Total COVID-19 Cases')
plt.ylabel('Cases (in millions)')
plt.grid(True, ls="-.")
plt.yticks(np.arange(min(y), max(y) + 10))
plt.plot(x, y, color="b")
plt.savefig('plots/US_Total_COVID-19_Cases.png')
# Plot Total Deaths
y = total_deaths / 1000
plt.figure('US Total COVID-19 Deaths', figsize=(15, 8))
plt.title('US Total COVID-19 Deaths')
plt.ylabel('Deaths (in thousands)')
plt.grid(True, ls="-.")
plt.yticks(np.arange(min(y), max(y) + 100, 50))
plt.plot(x, y, color="b")
plt.savefig('plots/US_Total_COVID-19_Deaths.png')
# Plot New Cases
y = new_cases / 1000
plt.figure('US New COVID-19 Cases', figsize=(15, 8))
plt.title('US New COVID-19 Cases')
plt.ylabel('Cases (in thousands)')
plt.grid(True, ls="-.")
plt.yticks(np.arange(min(y), max(y) + 100, 50))
plt.plot(x, y, color="b")
plt.savefig('plots/US_New_COVID-19_Cases.png')
# Plot New Deaths
y = new_deaths
plt.figure('US New COVID-19 Deaths', figsize=(15, 8))
plt.title('US New COVID-19 Deaths')
plt.ylabel('Deaths')
plt.grid(True, ls="-.")
plt.yticks(np.arange(min(y), max(y) + 1000, 500))
plt.plot(x, y, color="b")
plt.savefig('plots/US_New_COVID-19_Deaths.png')
# **** Write to README.md ****
# New cases and deaths in the last 24 hours
cases = new_cases[-1]
deaths = new_deaths[-1]
# 7-day mean for new cases and deaths
cmean = np.mean(new_cases[-7:])
dmean = np.mean(new_deaths[-7:])
# Date
date = np.array(date, dtype="datetime64")
date = date[-1]
# DataFrame for new cases and deaths in the last 24 hours
df_24 = pd.DataFrame({'New cases': [f'{cases:,d}'], 'New deaths': [f'{deaths:,d}']})
df_24 = df_24.to_markdown(index=False, disable_numparse=True)
# DataFrame for 7-day average
df_avg = pd.DataFrame({'Cases': [f'{int(cmean):,d}'], 'Deaths': [f'{int(dmean):,d}']})
df_avg = df_avg.to_markdown(index=False, disable_numparse=True)
# Write to 'README.md'
f = open('README.md', 'w')
f.write(f'''# US COVID-19 [Data](https://github.com/drebrb/covid-19-data/blob/master/data/us_covid-19_data.csv)
###### Reported numbers for {str(date)}
{df_24}
###### 7-day average
{df_avg}
## [Total Cases and Deaths](https://github.com/drebrb/covid-19-data/blob/master/data/us_covid-19_total.csv)
### Cases
![Plot](https://github.com/drebrb/covid-19-data/blob/master/plots/US_Total_COVID-19_Cases.png)
### Deaths
![Plot](https://github.com/drebrb/covid-19-data/blob/master/plots/US_Total_COVID-19_Deaths.png)
## [New Cases and Deaths](https://github.com/drebrb/covid-19-data/blob/master/data/us_covid-19_new.csv)
### Cases
![Plot](https://github.com/drebrb/covid-19-data/blob/master/plots/US_New_COVID-19_Cases.png)
### Deaths
![Plot](https://github.com/drebrb/covid-19-data/blob/master/plots/US_New_COVID-19_Deaths.png)''')
f.close()
# **** push to github ****
os.system('git add . && git commit -m "Updating data." && git push')
```
2 ответа
Ваш код в настоящее время представляет собой линейную последовательность шагов. Даже для довольно коротких программ это негибкая структура, с которой сложно экспериментировать, отлаживать, тестировать и развивать. Решение состоит в том, чтобы разбить ваш код на отдельные функции, каждая из которых имеет узкую направленность. Вот примерный набросок. Основная идея — разместить весь код (кроме импорта и констант) внутри функций.
# Imports and constants.
import sys
...
DATA_URL = 'https://raw.githubusercontent.com/nytimes/covid-19-data/master/us.csv'
# The program entry point
def main(args):
...
# Functions to do various things.
def fetch_data():
...
def write_csv_file(():
...
# Call main().
if __name__ == '__main__':
main(sys.arvg[1:])
Внесение этого изменения мгновенно откроет другие возможности. Например, вы можете захотеть добавить новое поведение в будущем. Это новое поведение можно легко добавить, не требуя повторного посещения всей программы. Вместо этого вы можете просто использовать аргумент командной строки (args
на скетче выше), чтобы запустить новое поведение.
Ваш код также имеет некоторую повторяемость — например, создание файла CSV и создание графика. Решение снова включает использование функций: определение повторяющихся частей; хранить параметры, которые меняются в структуре данных; и переместите повторяющееся действие в функцию. Например, для создания файла CSV вы можете рассмотреть что-то вроде схем, приведенных ниже. Аналогичный набор тактик можно применить и к созданию сюжета.
def main(args):
...
write_csv_files(...)
def write_csv_files(date, total_cases, total_deaths, new_cases, new_deaths):
# Prepare the parameters that vary.
csv_cols = {
'date': date,
'total cases': total_cases,
'total deaths': total_deaths,
'new cases': new_cases,
'new deaths': new_deaths,
}
# A data structure to drive the iteration.
csv_file_params = {
'total': ['date', 'total cases', 'total deaths']
'new': ['date', 'new cases', 'new deaths'],
'data': ['date', 'total cases', 'total deaths', 'new cases', 'new deaths'],
}
# Iterate.
for suffix, col_names in csv_file_params.items():
file_path = f'data/us_covid-19_{suffix}.csv'
d = {nm : csv_cols[nm] for nm in col_names}
write_csv_file(file_path, d)
def write_csv_file(file_path, d, index=False):
df = pd.DataFrame(d)
df.to_csv(file_path, index=index)
В вашем коде появляется все больше переменных. Например, на скетче выше write_csv_files()
требует 5 аргументов. Это не страшно, но это своего рода предупредительный знак. Если эти переменные необходимо перемещать вместе в вашей программе, вы можете рассмотреть различные способы их объединения. Есть много вариантов, например dict
, а namedtuple
, или
dataclass
.
Большие многострочные строки мешают читаемости и пониманию кода. Вместо того, чтобы загромождать README
Написав логику с помощью гигантской строки, отделите скучный материал (большая часть текста, который никогда не меняется) от логики кодирования:
from textwrap import dedent
# Define the template as a constant.
README_TEMPLATE = dedent('''
# US COVID-19 [Data](https://github.com/drebrb/covid-19-data/blob/master/data/us_covid-19_data.csv)
###### Reported numbers for {}
{}
###### 7-day average
{}
## [Total Cases and Deaths](https://github.com/drebrb/covid-19-data/blob/master/data/us_covid-19_total.csv)
### Cases
![Plot](https://github.com/drebrb/covid-19-data/blob/master/plots/US_Total_COVID-19_Cases.png)
### Deaths
![Plot](https://github.com/drebrb/covid-19-data/blob/master/plots/US_Total_COVID-19_Deaths.png)
## [New Cases and Deaths](https://github.com/drebrb/covid-19-data/blob/master/data/us_covid-19_new.csv)
### Cases
![Plot](https://github.com/drebrb/covid-19-data/blob/master/plots/US_New_COVID-19_Cases.png)
### Deaths
![Plot](https://github.com/drebrb/covid-19-data/blob/master/plots/US_New_COVID-19_Deaths.png)
''')
def write_readme(date, df_24, df_avg):
with open('README.md', 'w') as fh:
fh.write(README_TEMPLATE.format(date, df_24, df_avg))
HTTP-запросы и git
операции могут потерпеть неудачу. Их следует завернуть в
try
блоки, отказы которых так или иначе обрабатываются, как уже отмечалось в хорошем обзоре от Reinderien.
Вам следует добавить requirements.txt
содержащий что-то вроде
tabulate
pandas
requests
numpy
matplotlib
Первое требование было скрыто и укусило меня, когда я попытался запустить ваш код.
Внедрите проверку ошибок в свой requests
call и позвольте ему обрабатывать кодировку за вас:
url="https://raw.githubusercontent.com/nytimes/covid-19-data/master/us.csv"
with requests.get(url) as response:
response.raise_for_status()
with io.StringIO(response.text) as download:
df = pd.read_csv(download)
Вы должны создать каталоги данных и графиков, если они не существуют, что-то вроде
data = Path('data')
if data.exists():
assert data.is_dir()
else:
data.mkdir()
# ...
df.to_csv(data / 'us_covid-19_total.csv', index=False)
Используйте диспетчер контекста для написания readme:
with open('README.md', 'w') as f:
f.write( # ...
Попробуйте переместить большой контент README в стиле heredoc в файл шаблона и использовать встроенный создание шаблонов средство.
Не звони os.system
, не используйте операторы оболочки и не объединяйте их с &&
. Вместо этого используйте check_call и звоните прямо в /usr/bin/git
.
- 1
Вау, спасибо. Это много отличной информации. Обязательно добавлю файл requirements.txt, извините за это! Что касается каталога данных и графиков, я сомневался в том, следует ли мне предупреждать пользователя, если каталоги отсутствуют, или я должен просто создать их. Мне нужно будет изучить шаблоны и check_call, поскольку я не знаком с ними, но я понимаю ваш блок вызова запроса и диспетчер контекста и буду их реализовывать. Спасибо, еще есть чему поучиться, лол.
— дребрб
Выбрасывание, если каталоги еще не существуют, тоже хороший вариант; все, что подходит для вашего варианта использования.
— Райндериен
Вау, спасибо большое. Кое-что из того, что вы объяснили, я ЕЩЕ не понимаю, так как я все еще учусь, но вы предоставили мне достаточно информации, чтобы я теперь исследовал и учился. Спасибо.
— дребрб