Some points
The example you gave is a bit simple and I believe make it a bit harder to think in a more generic case. I then generated random data for 30 days using numpy.
By seeing the link you sent, I think they're showing us "how many days
is the latest day that current day is double of apart from current_day".
To show this explicitly I will use very verbose column names in pandas and
before calculating the metrics you want, I will build in the dataframe a reference list called days_current_day_is_double_of
wich will for each row(day) calculate a list of days which the current deaths_cum is double of the day deaths_cum.
This column later can be substituted for a simple np.where() operation every time you want to find this for a row, if you don't want to keep a reference list in the dataframe. I think it's clearer keeping it.
generating data
import pandas as pd
import numpy as np
import io
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)
#n_of_days = 30
#random_data = np.random.randint(0,100,size=n_of_days)
#date_range = pd.date_range(start="2020-03-02",freq="D",periods=n_of_days)
#random_data = pd.DataFrame({"deaths":random_data})
#random_data.index = pd.to_datetime(date_range)
#df= random_data
import requests
import json
response = requests.get("https://api-covid.unthinkingdepths.fr/covid19/ecdc?type=cum")
data = json.loads(response.text)["data"]
deaths_cums = [x["deaths_cum"] for x in data]
dates = [x["dateRep"] for x in data]
df = pd.DataFrame({"deaths_cum":deaths_cums})
df.index = pd.to_datetime(dates)
A verbose solution in pandas
The key here is :
- using apply(axis=1) to iterate over rows,
using apply() to iterate over columns
use np.where to do backwards search explicitly
I use np.where inside the helper function check_condition(row)
to create the days references once and then use find_index(list_of_days, idx)
to search again anytime
- lambda functions but organize them with "helper functions"
big picture of the code
# create helper functions
def check_condition(row):
+--- 7 lines: ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
def delta_fromlast_day_currDay_is_double_of(row):
+--- 12 lines: ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
def how_many_days_fromlast_day_currDay_is_double_of(row):
+--- 11 lines: ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
def find_index(list_of_days,index):
+--- 4 lines: {-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
# use apply here with lambda functions
+--- 23 lines: df['deaths_cum'] = np.cumsum(df['deaths'])------------------------------------------------------------------------------------------------------------------------------------------------
print(df)
Full solution code
def check_condition(row):
row_idx = df.index.get_loc(row.name)
currRow_deaths_cum = df.iloc[row_idx]['deaths_cum']
rows_before_current_deaths_cum = df.iloc[:row_idx]['deaths_cum']
currRow_is_more_thanDobuleOf = np.where((currRow_deaths_cum/rows_before_current_deaths_cum) >= 2)[0]
return currRow_is_more_thanDobuleOf
def delta_fromlast_day_currDay_is_double_of(row):
row_idx = df.index.get_loc(row.name)
currRow_deaths_cum = df.iloc[row_idx]['deaths_cum']
list_of_days = df.iloc[row_idx]['days_current_day_is_double_of']
last_day_currDay_is_double_of = find_index(list_of_days,-1)
if last_day_currDay_is_double_of is np.nan:
delta = np.nan
else:
last_day_currDay_is_double_of_deaths_cum = df.iloc[last_day_currDay_is_double_of]["deaths_cum"]
delta = currRow_deaths_cum - last_day_currDay_is_double_of_deaths_cum
return delta
def how_many_days_fromlast_day_currDay_is_double_of(row):
row_idx = df.index.get_loc(row.name)
list_of_days = df.iloc[row_idx]['days_current_day_is_double_of']
last_day_currDay_is_double_of = find_index(list_of_days,-1)
if last_day_currDay_is_double_of is np.nan:
delta = np.nan
else:
delta = row_idx - last_day_currDay_is_double_of
return delta
def find_index(list_of_days,index):
if list_of_days.any(): return list_of_days[index]
else: return np.nan
# use apply here with lambda functions
#df['deaths_cum'] = np.cumsum(df['deaths'])
df['deaths_cum_ratio_from_day0'] = df['deaths_cum'].apply(
lambda cum_deaths: cum_deaths/df['deaths_cum'].iloc[0]
if df['deaths_cum'].iloc[0] != 0
else np.nan
)
#df['increase_in_deaths_cum'] = df['deaths_cum'].diff().cumsum() <- this mmight be interesting for you to use for other analyses
df['days_current_day_is_double_of'] = df.apply(
lambda row:check_condition(row),
axis=1
)
df['first_day_currDay_is_double_of'] = df['days_current_day_is_double_of'].apply(lambda list_of_days: find_index(list_of_days,0))
df['last_day_currDay_is_double_of'] = df['days_current_day_is_double_of'].apply(lambda list_of_days: find_index(list_of_days,-1))
df['delta_fromfirst_day'] = df['deaths_cum'] - df['deaths_cum'].iloc[0]
df['delta_fromlast_day_currDay_is_double_of'] = df.apply(
lambda row: delta_fromlast_day_currDay_is_double_of(row),
axis=1
)
df['how_many_days_fromlast_day_currDay_is_double_of'] = df.apply(
lambda row: how_many_days_fromlast_day_currDay_is_double_of(row),
axis=1
)
print(df[-30:])
PANDAS SOLUTION OUTPUT
deaths_cum deaths_cum_ratio_from_day0 \
2020-03-22 562 NaN
2020-03-23 674 NaN
2020-03-24 860 NaN
2020-03-25 1100 NaN
2020-03-26 1331 NaN
2020-03-27 1696 NaN
2020-03-28 1995 NaN
2020-03-29 2314 NaN
2020-03-30 2606 NaN
2020-03-31 3024 NaN
2020-04-01 3523 NaN
2020-04-02 4032 NaN
2020-04-03 4503 NaN
2020-04-04 6507 NaN
2020-04-05 7560 NaN
2020-04-06 8078 NaN
2020-04-07 8911 NaN
2020-04-08 10328 NaN
2020-04-09 10869 NaN
2020-04-10 12210 NaN
2020-04-11 13197 NaN
2020-04-12 13832 NaN
2020-04-13 14393 NaN
2020-04-14 14967 NaN
2020-04-15 15729 NaN
2020-04-16 17167 NaN
2020-04-17 17920 NaN
2020-04-18 18681 NaN
2020-04-19 19323 NaN
2020-04-20 19718 NaN
days_current_day_is_double_of \
2020-03-22 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-03-23 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-03-24 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-03-25 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-03-26 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-03-27 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-03-28 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-03-29 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-03-30 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-03-31 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-01 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-02 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-03 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-04 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-05 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-06 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-07 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-08 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-09 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-10 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-11 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-12 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-13 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-14 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-15 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-16 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-17 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-18 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-19 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
2020-04-20 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,...
first_day_currDay_is_double_of last_day_currDay_is_double_of \
2020-03-22 0.0 79.0
2020-03-23 0.0 79.0
2020-03-24 0.0 80.0
2020-03-25 0.0 81.0
2020-03-26 0.0 82.0
2020-03-27 0.0 83.0
2020-03-28 0.0 84.0
2020-03-29 0.0 85.0
2020-03-30 0.0 85.0
2020-03-31 0.0 86.0
2020-04-01 0.0 87.0
2020-04-02 0.0 88.0
2020-04-03 0.0 88.0
2020-04-04 0.0 91.0
2020-04-05 0.0 92.0
2020-04-06 0.0 93.0
2020-04-07 0.0 93.0
2020-04-08 0.0 94.0
2020-04-09 0.0 94.0
2020-04-10 0.0 94.0
2020-04-11 0.0 95.0
2020-04-12 0.0 95.0
2020-04-13 0.0 95.0
2020-04-14 0.0 95.0
2020-04-15 0.0 96.0
2020-04-16 0.0 97.0
2020-04-17 0.0 98.0
2020-04-18 0.0 98.0
2020-04-19 0.0 98.0
2020-04-20 0.0 98.0
delta_fromfirst_day delta_fromlast_day_currDay_is_double_of \
2020-03-22 562 318.0
2020-03-23 674 430.0
2020-03-24 860 488.0
2020-03-25 1100 650.0
2020-03-26 1331 769.0
2020-03-27 1696 1022.0
2020-03-28 1995 1135.0
2020-03-29 2314 1214.0
2020-03-30 2606 1506.0
2020-03-31 3024 1693.0
2020-04-01 3523 1827.0
2020-04-02 4032 2037.0
2020-04-03 4503 2508.0
2020-04-04 6507 3483.0
2020-04-05 7560 4037.0
2020-04-06 8078 4046.0
2020-04-07 8911 4879.0
2020-04-08 10328 5825.0
2020-04-09 10869 6366.0
2020-04-10 12210 7707.0
2020-04-11 13197 6690.0
2020-04-12 13832 7325.0
2020-04-13 14393 7886.0
2020-04-14 14967 8460.0
2020-04-15 15729 8169.0
2020-04-16 17167 9089.0
2020-04-17 17920 9009.0
2020-04-18 18681 9770.0
2020-04-19 19323 10412.0
2020-04-20 19718 10807.0
how_many_days_fromlast_day_currDay_is_double_of
2020-03-22 3.0
2020-03-23 4.0
2020-03-24 4.0
2020-03-25 4.0
2020-03-26 4.0
2020-03-27 4.0
2020-03-28 4.0
2020-03-29 4.0
2020-03-30 5.0
2020-03-31 5.0
2020-04-01 5.0
2020-04-02 5.0
2020-04-03 6.0
2020-04-04 4.0
2020-04-05 4.0
2020-04-06 4.0
2020-04-07 5.0
2020-04-08 5.0
2020-04-09 6.0
2020-04-10 7.0
2020-04-11 7.0
2020-04-12 8.0
2020-04-13 9.0
2020-04-14 10.0
2020-04-15 10.0
2020-04-16 10.0
2020-04-17 10.0
2020-04-18 11.0
2020-04-19 12.0
2020-04-20 13.0
If you check how_many_days_fromlast_day_currDay_is_double_of
matches exactly with XDelta
from the api :)
There are so many small suggestions in case you want to really generalize your code. I don't think that's what you're looking for but I will list some:
- you can easily add a growth factor in the check_growth_condition function :
def check_growth_condition(row, growth_factor):
....
np.where((currRow_deaths_cum/rows_before_current_deaths_cum) >= growth_factor)[0] # <----- then just change 2 by the growth factor
....
- you could reduce the reference list of
days current day is double of
to just the lastest date current day is double of, because all days before the lastest will also be double the ratio. I'll keep the first and last just for the sake of showing a "range of days".
def check_growth_condition(row, growth_factor):
...
# doing backwards search with np.where
currRow_is_more_thanDoubleOf = np.where((currRow_deaths_cum/rows_before_current_deaths_cum) >= growth_factor)[0]
if currRow_is_more_thanDobuleOf.any():
return np.array([currRow_is_more_thanDobuleOf[0],currRow_is_more_thanDobuleOf[-1]]) # <------ return just first and last
else:
return currRow_is_more_thanDobuleOf # empty list
...
Note also if you want to get rid of the reference column, you just need to use np.where((currRow_deaths_cum/rows_before_current_deaths_cum) >= growth_factor)[0]
wherever I am using the check_growth_condition
function. again np.where is always doing the searching.
- if you want to generalize deltas between current day to any other day for any columns, just pass day_idx and column name as parameter. you could even generalize
delta_from_any_day
instead of just subtract you pass a function as input such as np.divide
to calculate ratios or np.subtract
to calulate the deltas as I'm doing in the example
def delta_from_any_day(row, day_idx,
column_name='deaths_cum',func=np.subtract):
row_idx = df.index.get_loc(row.name)
currRow_deaths_cum = df.iloc[row_idx][column_name]
if day_idx is np.nan:
delta = np.nan
else:
day_idx_deaths_cum = df.iloc[day_idx][column_name]
delta = func(currRow_deaths_cum, day_idx_deaths_cum)
return delta
Cleaner Pandas solution
note that we're just reusing check_growth_condition
,find_index
to do backsearching and delta_from_any_day
and to calculate the deltas. We're just reusing those three in all other helper function to calculate stuff.
def check_growth_condition(row, growth_factor):
row_idx = df.index.get_loc(row.name)
currRow_deaths_cum = df.iloc[row_idx]['deaths_cum']
rows_before_current_deaths_cum = df.iloc[:row_idx]['deaths_cum']
currRow_is_more_thanDoubleOf = np.where((currRow_deaths_cum/rows_before_current_deaths_cum) >= growth_factor)[0]
if currRow_is_more_thanDoubleOf.any():
return np.array([currRow_is_more_thanDoubleOf[0], currRow_is_more_thanDoubleOf[-1]])
else:
return currRow_is_more_thanDoubleOf # empty list
def find_index(list_of_days,index):
if list_of_days.any(): return list_of_days[index]
else: return np.nan
def delta_from_any_day(row, day_idx, column_name='deaths_cum',func=np.subtract):
row_idx = df.index.get_loc(row.name)
currRow_deaths_cum = df.iloc[row_idx][column_name]
if day_idx is np.nan:
delta = np.nan
else:
day_idx_deaths_cum = df.iloc[day_idx][column_name]
delta = func(currRow_deaths_cum, day_idx_deaths_cum)
return delta
def delta_fromlast_day_currDay_is_double_of(row):
row_idx = df.index.get_loc(row.name)
currRow_deaths_cum = df.iloc[row_idx]['deaths_cum']
list_of_days = df.iloc[row_idx]['rangeOf_days_current_day_is_double_of']
last_day_currDay_is_double_of = find_index(list_of_days,-1)
delta = delta_from_any_day(row, last_day_currDay_is_double_of, column_name="deaths_cum")
return delta
def how_many_days_fromlast_day_currDay_is_double_of(row):
row_idx = df.index.get_loc(row.name)
list_of_days = df.iloc[row_idx]['rangeOf_days_current_day_is_double_of']
last_day_currDay_is_double_of = find_index(list_of_days,-1)
delta = delta_from_any_day(row, last_day_currDay_is_double_of, column_name="day_index")
return delta
# use apply here with lambda functions
#df['deaths_cum'] = np.cumsum(df['deaths'])
#df['deaths_cum_ratio_from_day0'] = df['deaths_cum'].apply(
# lambda cum_deaths: cum_deaths/df['deaths_cum'].iloc[0]
# if df['deaths_cum'].iloc[0] != 0
# else np.nan
# )
#df['increase_in_deaths_cum'] = df['deaths_cum'].diff().cumsum() <- this mmight be interesting for you to use for other analyses
df['rangeOf_days_current_day_is_double_of'] = df.apply(
lambda row:check_growth_condition(row,2),
axis=1
)
df['first_day_currDay_is_double_of'] = df['rangeOf_days_current_day_is_double_of'].apply(lambda list_of_days: find_index(list_of_days,0))
df['last_day_currDay_is_double_of'] = df['rangeOf_days_current_day_is_double_of'].apply(lambda list_of_days: find_index(list_of_days,-1))
df['delta_fromfirst_day'] = df['deaths_cum'] - df['deaths_cum'].iloc[0]
df['delta_fromlast_day_currDay_is_double_of'] = df.apply(
lambda row: delta_fromlast_day_currDay_is_double_of(row),
axis=1
)
df['how_many_days_fromlast_day_currDay_is_double_of'] = df.apply(
lambda row: how_many_days_fromlast_day_currDay_is_double_of(row),
axis=1
)
print(df[-5:])
Clean Output
day_index deaths_cum rangeOf_days_current_day_is_double_of \
2020-04-16 107 17167 [0, 97]
2020-04-17 108 17920 [0, 98]
2020-04-18 109 18681 [0, 98]
2020-04-19 110 19323 [0, 98]
2020-04-20 111 19718 [0, 98]
first_day_currDay_is_double_of last_day_currDay_is_double_of \
2020-04-16 0.0 97.0
2020-04-17 0.0 98.0
2020-04-18 0.0 98.0
2020-04-19 0.0 98.0
2020-04-20 0.0 98.0
delta_fromfirst_day delta_fromlast_day_currDay_is_double_of \
2020-04-16 17167 9089.0
2020-04-17 17920 9009.0
2020-04-18 18681 9770.0
2020-04-19 19323 10412.0
2020-04-20 19718 10807.0
how_many_days_fromlast_day_currDay_is_double_of
2020-04-16 10.0
2020-04-17 10.0
2020-04-18 11.0
2020-04-19 12.0
2020-04-20 13.0