Displaying COVID-19 deaths per country per day on a rotating 3D Earth.
An animation displays on a 3D Earth the total number of deaths for each day. They are represented in log scale as cones positionned on capital's locations of each country. During the animation, the 3D Earth is rotating around its own axis eastward to enable the user to see the spread of the pandemic. The speed of the rotation is not related to the speed of Earth's rotation.
The outcome figure itself intends to look like the coronavirus as a sphere with spikes.
Data sources:
Limitations of the data visualization:
Author: Francis Wolinski
The visualization relies on a few Python libraries.
# imports
import numpy as np
import pandas as pd
import ipyvolume as ipv
import shapefile
import pythreejs
# version of the used modules
for m in [np, pd, ipv, shapefile, pythreejs]:
try:
print(m.__name__, m.__version__)
except AttributeError:
print(m.__name__, m._version.__version__)
# load COVID data for 2020
df_covid = pd.read_csv('data/owid-covid-data.csv')
df_covid = df_covid.loc[df_covid['date'].str.startswith('2020')]
df_covid.head(3)
# load country data with ISO 2 and 3 codes
var = pd.read_html('data/geonames.htm')
df_countries = var[1]
df_countries = df_countries[['ISO-3166alpha2', 'ISO-3166alpha3', 'Country']]
df_countries.head(3)
# load city data with longitude and latitude of capitals (type == 'PPLC')
df_capitals = pd.read_csv('data/cities15000.txt',
sep='\t',
header=None,
usecols=[4, 5, 7, 8],
names=['lat', 'long', 'type', 'ISO-3166alpha2'])
df_capitals = df_capitals.loc[df_capitals['type'] == 'PPLC']
df_capitals.head(3)
# load shape file with coastlines
sf = shapefile.Reader('ne_110m_coastline/ne_110m_coastline.shp')
# This function adds to a DataFrame 3D point coordinates
# computed from longitude and latitude in degrees.
# It relies on a spherical model which simplifies the actual shape of the Earth.
# It also perfoms a permutation between axis (x -> -x, y -> z, z -> y)
# Used for capitals as well as coastlines
degres_to_radians = np.pi / 180.0
def compute_xyz(df):
long_in_radians = df['long'] * degres_to_radians
lat_in_radians = df['lat'] * degres_to_radians
df['x'] = - np.cos(long_in_radians) * np.cos(lat_in_radians)
df['y'] = np.sin(lat_in_radians)
df['z'] = np.sin(long_in_radians) * np.cos(lat_in_radians)
# merge countries and capitals using ISO 2 codes
df_geo = pd.merge(df_countries,
df_capitals,
on='ISO-3166alpha2',
how='inner')
df_geo.head(3)
# table with total deaths by country and day
col = 'total_deaths'
df_pandemic = df_covid.pivot_table(index='iso_code',
columns='date',
values=col)
df_pandemic = df_pandemic.fillna(0).astype(int)
df_pandemic = df_pandemic.fillna(1.0).clip(1.0, None).apply(np.log)
df_pandemic.head(3)
The final DataFrame contains one column per day (from 2020-01-01 to 2020-05-31) with the total deaths per country and the appropriate 3D positions of the capital of each country.
# merge total deaths and geo data using ISO 3 codes
df_pandemic = pd.merge(df_pandemic.reset_index(),
df_geo,
left_on='iso_code',
right_on='ISO-3166alpha3',
how='inner')
compute_xyz(df_pandemic)
df_pandemic.head(3)
The final figure is plotted in few steps:
# plot the figure
fig = ipv.figure()
# total deaths data are in columns whose name starts with '2020'
cols = [col for col in df_pandemic.columns if col.startswith('2020')]
# plot the total deaths (log scale) as red arrows around the 3D Earth
# all data are fixed except the size which will change at each step
xs = df_pandemic['x'].values
ys = df_pandemic['y'].values
zs = df_pandemic['z'].values
s = ipv.scatter(xs * 1.05, ys * 1.05, zs * 1.05, # factor to move the arrow out of the 3D Earth
vx=xs, vy=ys, vz=zs,
size=df_pandemic[cols].T,
color='darkred', marker="arrow")
# plot the coastlines
for shape in sf.shapes():
df = pd.DataFrame(shape.points, columns=['long', 'lat'])
compute_xyz(df)
xs = df['x'].values
ys = df['y'].values
zs = df['z'].values
ipv.pylab.plot(xs, ys, zs, color='darkgrey')
# display parameters
ipv.xyzlim(1)
ipv.style.use('minimal')
animation_control = ipv.animation_control(s, interval=200, sequence_length=len(cols))
ipv.show()
# control parameters
control = pythreejs.OrbitControls(controlling=fig.camera)
fig.controls = control
control.rotateSpeed = 0.07
control.autoRotate = True
fig.render_continuous = True

The animated GIF file has been produced thanks to the LICEcap software, see: https://www.cockos.com/licecap/