In [1]:
import numpy as np
import pandas as pd
import json
import re
import os
import math
import random 
from collections import Counter
import matplotlib.pyplot as plt
%matplotlib inline

Data load & format¶

In [2]:
# Define directories
homedir = os.path.expanduser("~")
projdir = os.path.join(homedir,'Documents','Research','Trajectories')
datadir = os.path.join(projdir,'data_mm')
figsdir = os.path.join(projdir,'figures')
In [3]:
# Load into Pandas
mm_motifs3_fm = os.path.join(datadir,"c3_motifs.csv")
mm_motifs4_fm = os.path.join(datadir,"c4_motifs.csv")
mm_motifs3 = pd.read_csv(mm_motifs3_fm).drop(columns=['users'])
mm_motifs4 = pd.read_csv(mm_motifs4_fm).drop(columns=['users'])

mm_savings_fn = os.path.join(datadir,"users_savings.csv")
mm_savings = pd.read_csv(mm_savings_fn).sort_values(["user_ID","days"]).reset_index(drop=True)
In [4]:
# Convert amount to USD at PPP in 2016
ppp = XXXX
mm_motifs3["amount"] = mm_motifs3.amount.divide(ppp)
mm_motifs4["amount"] = mm_motifs4.amount.divide(ppp)
mm_savings["amt"] = mm_savings.amt.divide(ppp)
mm_savings["amt_c"] = mm_savings.amt_c.divide(ppp)
In [5]:
mm_motifs3.head()
Out[5]:
motif flows amount deposits median_dur_f median_dur_a median_dur_d
0 cash-dep~mins-pay 10196736 2.325440e+07 502056.653378 183.960278 164.578889 79.512222
1 bank-dep~mins-pay 200827 5.725882e+05 17202.420586 160.373611 139.261944 64.001667
2 cash-dep~circulate~mins-pay 5368800 1.153680e+07 96925.287974 302.427778 276.202778 152.056111
3 cash-dep~cash-pay 1018088 1.205092e+08 525438.514383 10.167222 1.463889 0.682778
4 cash-dep~bill-pay 999223 7.990287e+07 442826.425598 1.418333 0.133889 0.172222
In [6]:
mm_motifs4.head()
Out[6]:
motif flows amount deposits median_dur_f median_dur_a median_dur_d
0 cash-dep~mins-pay 10196736 2.325440e+07 502056.653378 183.960278 164.578889 79.512222
1 bank-dep~mins-pay 200827 5.725882e+05 17202.420586 160.373611 139.261944 64.001667
2 cash-dep~p2p~mins-pay 3560416 7.705147e+06 69691.759336 262.812222 239.596944 118.643333
3 cash-dep~cash-pay 1018088 1.205092e+08 525438.514383 10.167222 1.463889 0.682778
4 cash-dep~bill-pay 999223 7.990287e+07 442826.425598 1.418333 0.133889 0.172222
In [7]:
mm_savings.head()
Out[7]:
user_ID days amt txn flw amt_c txn_c flw_c amt_cr txn_cr flw_cr
0 5 0.0 3.892683 0.19172 3 87.108014 4.00000 44 1.000000 1.00000 1.000000
1 5 1.0 1.045296 0.06000 1 83.215331 3.80828 41 0.955312 0.95207 0.931818
2 5 2.0 2.787456 0.13000 2 82.170035 3.74828 40 0.943312 0.93707 0.909091
3 5 3.0 2.090592 0.09000 2 79.382578 3.61828 38 0.911312 0.90457 0.863636
4 5 4.0 2.090592 0.12000 2 77.291986 3.52828 36 0.887312 0.88207 0.818182

Table¶

In [8]:
# Use c3, but split off the sterotypical transfer motif
mm_motifs = mm_motifs3[~(mm_motifs3['motif']=='cash-dep~circulate~cash-wtd')]
mm_motifs = mm_motifs.append(mm_motifs4[mm_motifs4['motif']=='cash-dep~circulate~cash-wtd'])
mm_motifs = mm_motifs.append(mm_motifs4[mm_motifs4['motif']=='cash-dep~p2p~cash-wtd'])
In [9]:
# Calculate the total
mm_motifs = mm_motifs.append(mm_motifs.sum(numeric_only=True), ignore_index=True)
mm_motifs.loc[mm_motifs.index[-1],'motif'] = "total"
mm_motifs[mm_motifs['motif']=="total"]
Out[9]:
motif flows amount deposits median_dur_f median_dur_a median_dur_d
50 total 25275555.0 9.653706e+08 4.093113e+06 20761.01 22427.7675 19721.8925
In [10]:
# Normalize the relevant columns
mm_motifs['amount_frac'] = mm_motifs['amount'].apply(lambda x: x/mm_motifs.loc[mm_motifs.index[-1],'amount'])
mm_motifs['deposits_frac'] = mm_motifs['deposits'].apply(lambda x: x/mm_motifs.loc[mm_motifs.index[-1],'deposits'])
mm_motifs['flows_frac'] = mm_motifs['flows'].apply(lambda x: x/mm_motifs.loc[mm_motifs.index[-1],'flows'])
In [11]:
# Get the total share of ~single-use~ funds
deposits = ["cash-dep","bank-dep"]
exits = ["cash-wtd","bank-wtd","cash-pay","bill-pay","mins-pay"]
single_use = ["cash-dep~p2p~cash-wtd"]+[dep+"~"+wtd for dep in deposits for wtd in exits]
one = sum(mm_motifs[mm_motifs["motif"].isin(single_use)]["amount_frac"])
one
Out[11]:
0.7297743440519162
In [12]:
# Get the total ~circulating~ funds
circulating = [dep+"~circulate~"+wtd for dep in deposits for wtd in exits]
circ = sum(mm_motifs[mm_motifs["motif"].isin(circulating)]["amount_frac"])
circ
Out[12]:
0.19681264581654326
In [13]:
# Get the total ~remaining~ funds
rem = sum(mm_motifs[mm_motifs["motif"].str.contains("inferred")]["amount_frac"])
rem
Out[13]:
0.07341107159385352
In [14]:
# Confirm total
one + circ + rem
Out[14]:
0.999998061462313
In [15]:
# Avoid smaller and idiosyncratic services
mm_motifs_filtered = mm_motifs[mm_motifs["flows_frac"]>0.01]
In [16]:
mm_motifs_filtered
Out[16]:
motif flows amount deposits median_dur_f median_dur_a median_dur_d amount_frac deposits_frac flows_frac
0 cash-dep~mins-pay 10196736.0 2.325440e+07 5.020567e+05 183.960278 164.578889 79.512222 0.024089 0.122659 0.403423
2 cash-dep~circulate~mins-pay 5368800.0 1.153680e+07 9.692529e+04 302.427778 276.202778 152.056111 0.011951 0.023680 0.212411
3 cash-dep~cash-pay 1018088.0 1.205092e+08 5.254385e+05 10.167222 1.463889 0.682778 0.124832 0.128371 0.040280
4 cash-dep~bill-pay 999223.0 7.990287e+07 4.428264e+05 1.418333 0.133889 0.172222 0.082769 0.108188 0.039533
5 cash-dep~cash-wtd 1489837.0 2.229407e+08 7.543074e+05 90.571944 20.618056 23.076111 0.230938 0.184287 0.058944
7 cash-dep~circulate~cash-pay 622752.0 4.080594e+07 1.727622e+05 121.379722 58.830278 55.387222 0.042270 0.042208 0.024639
12 cash-dep~circulate~bill-pay 829361.0 4.078656e+07 2.017926e+05 142.340278 76.928889 73.011667 0.042250 0.049301 0.032813
42 cash-dep~circulate~inferred 824899.0 2.002063e+07 7.257145e+04 2718.515833 2961.969167 3034.411389 0.020739 0.017730 0.032636
43 cash-dep~inferred 920826.0 4.989388e+07 1.679239e+05 2605.905556 3710.645278 3036.593056 0.051684 0.041026 0.036431
48 cash-dep~circulate~cash-wtd 910000.0 8.533672e+07 2.744732e+05 196.696111 66.808333 80.207500 0.088398 0.067057 0.036003
49 cash-dep~p2p~cash-wtd 1486837.0 2.272370e+08 7.097156e+05 67.764444 24.050278 23.789722 0.235388 0.173393 0.058825
50 total 25275555.0 9.653706e+08 4.093113e+06 20761.010000 22427.767500 19721.892500 1.000000 1.000000 1.000000

Figure¶

In [17]:
# Let's limit ourselves to users with three or more transactions of turnover
consistent_users = mm_savings[mm_savings['txn_c']>=3]['user_ID'].unique()
n_consistent_users = len(consistent_users)

mm_savings_consistent = mm_savings[mm_savings['user_ID'].isin(consistent_users)]
In [18]:
# Get the longest save duration, by various cutoffs, for each user
def max_save_duration(df, weight="amt_c", min_val=0):
    label = weight+">"+str(min_val)
    return df[df[weight]>min_val].groupby('user_ID').agg({'days':max}).squeeze().rename(label)
In [19]:
# Amount cutoffs
cutoffs = {
    "amt_c"  : [0,1,10,100,1000],
    "txn_c"  : [0,0.5,1,2,5],
    "amt_cr" : [0,0.01,0.05,0.20]
          }
cutoff_list = []
for term in cutoffs:
    cutoff_list = cutoff_list+[(term,val) for val in cutoffs[term]]
In [20]:
mm_savings_user = pd.concat([max_save_duration(mm_savings_consistent,weight=term,min_val=val) for term, val in cutoff_list],axis=1)
In [21]:
mm_savings_user.head()
Out[21]:
amt_c>0 amt_c>1 amt_c>10 amt_c>100 amt_c>1000 txn_c>0 txn_c>0.5 txn_c>1 txn_c>2 txn_c>5 amt_cr>0 amt_cr>0.01 amt_cr>0.05 amt_cr>0.2
user_ID
5 31.0 31.0 31.0 NaN NaN 31.0 31.0 31.0 17.0 NaN 31.0 31.0 31.0 31.0
17 31.0 31.0 26.0 2.0 0.0 31.0 17.0 17.0 0.0 0.0 31.0 24.0 2.0 2.0
18 31.0 31.0 3.0 NaN NaN 31.0 31.0 31.0 3.0 NaN 31.0 31.0 31.0 31.0
28 31.0 31.0 31.0 31.0 9.0 31.0 31.0 27.0 9.0 2.0 31.0 31.0 31.0 6.0
35 31.0 31.0 31.0 31.0 4.0 31.0 31.0 19.0 8.0 4.0 31.0 31.0 12.0 4.0
In [22]:
# Summarize by day
mm_savings_days = mm_savings_user.apply(lambda x: x.value_counts())
mm_savings_days = mm_savings_days.apply(lambda x: x/n_consistent_users)

Plot savings figure¶

In [23]:
# Plot the number of users who saved more than zero for longer than X days
fig = plt.figure()
ax = fig.add_axes([0,0,1,1])
ax.bar(mm_savings_days.index,mm_savings_days.loc[::-1,'amt_cr>0.05'].cumsum()[::-1])
plt.show()

Publication quality plot¶

In [24]:
# Figure
fig = plt.figure(dpi=200,figsize=(4,3))
ax = fig.add_axes([0,0,1,1],frameon=False)

# Savings accumulation duration label
save_lengths = [str(dur) for dur in range(31)]+['31+']

# Plotted series
savings_gt0 = mm_savings_days.loc[::-1,'amt_cr>0'].cumsum()[::-1]
savings_gt5 = mm_savings_days.loc[::-1,'amt_cr>0.01'].cumsum()[::-1]
savings_gt10 = mm_savings_days.loc[::-1,'amt_cr>0.05'].cumsum()[::-1]
savings_gt25 = mm_savings_days.loc[::-1,'amt_cr>0.2'].cumsum()[::-1]

ax.plot(save_lengths,savings_gt0,marker='o',color='firebrick',linewidth=0.5,label='>  0%')
ax.plot(save_lengths,savings_gt5,marker='8',color='teal',linewidth=0.5,label='>  1%')
ax.plot(save_lengths,savings_gt10,marker='h',color='skyblue',linewidth=0.5,label='>  5%')
ax.plot(save_lengths,savings_gt25,marker='p',color='gold',linewidth=0.5,label='>20%')

# Percentages on y-axis
ax.set_ylim(0,1.05)
ax.grid(axis='y', color='lightgrey', linestyle='--', linewidth=0.75)
y_vals = ax.get_yticks()
ax.set_yticklabels(['{:,.0%}'.format(x) for x in y_vals])
ax.set_ylabel('Share of accounts', fontsize=12, labelpad=6)

# Days on x-axis
ax.set_axisbelow(True)
ax.grid(axis='x', color='lightgrey', linestyle='--', linewidth=0.75)
ax.set_xticks(['0','7','14','21','28','31+'])
ax.set_xlabel('Days since funds received', fontsize=12, labelpad=6)

# Legend for savings worth
ax.legend(title='Funds remaining:',loc='upper right',bbox_to_anchor=(1.1, 0.87),numpoints=2,borderaxespad=1)

# Save the plot!
plt.savefig(os.path.join(figsdir,'MM_accounts_save.pdf'),dpi=300, bbox_inches='tight')
In [25]:
# Figure
fig = plt.figure(dpi=200,figsize=(4,3))
ax = fig.add_axes([0,0,1,1],frameon=False)

# Savings accumulation duration label
save_lengths = [str(dur) for dur in range(31)]+['31+']

# Plotted series
savings_gt0 = mm_savings_days.loc[::-1,'amt_c>0'].cumsum()[::-1]
savings_gt5 = mm_savings_days.loc[::-1,'amt_c>1'].cumsum()[::-1]
savings_gt10 = mm_savings_days.loc[::-1,'amt_c>10'].cumsum()[::-1]
savings_gt25 = mm_savings_days.loc[::-1,'amt_c>100'].cumsum()[::-1]

ax.plot(save_lengths,savings_gt0,marker='o',color='firebrick',linewidth=0.5,label='>    0$PPP')
ax.plot(save_lengths,savings_gt5,marker='8',color='teal',linewidth=0.5,label='>    1$PPP')
ax.plot(save_lengths,savings_gt10,marker='h',color='skyblue',linewidth=0.5,label='>  10$PPP')
ax.plot(save_lengths,savings_gt25,marker='p',color='gold',linewidth=0.5,label='>100$PPP')

# Percentages on y-axis
ax.set_ylim(0,1.05)
ax.grid(axis='y', color='lightgrey', linestyle='--', linewidth=0.75)
y_vals = ax.get_yticks()
ax.set_yticklabels(['{:,.0%}'.format(x) for x in y_vals])
ax.set_ylabel('Share of accounts', fontsize=12, labelpad=6)

# Days on x-axis
ax.set_axisbelow(True)
ax.grid(axis='x', color='lightgrey', linestyle='--', linewidth=0.75)
ax.set_xticks(['0','7','14','21','28','31+'])
ax.set_xlabel('Days since funds received', fontsize=12, labelpad=6)

# Legend for savings worth
ax.legend(title='Funds remaining:',loc='upper right',bbox_to_anchor=(0.43, 0.43),numpoints=2,borderaxespad=1)

# Save the plot!
plt.savefig(os.path.join(figsdir,'MM_accounts_save_abs.pdf'),dpi=300, bbox_inches='tight')
In [26]:
# Figure
fig = plt.figure(dpi=200,figsize=(4,3))
ax = fig.add_axes([0,0,1,1],frameon=False)

# Savings accumulation duration label
save_lengths = [str(dur) for dur in range(31)]+['31+']

# Plotted series
savings_gt0 = mm_savings_days.loc[::-1,'txn_c>0'].cumsum()[::-1]
savings_gt5 = mm_savings_days.loc[::-1,'txn_c>0.5'].cumsum()[::-1]
savings_gt10 = mm_savings_days.loc[::-1,'txn_c>1'].cumsum()[::-1]
savings_gt25 = mm_savings_days.loc[::-1,'txn_c>2'].cumsum()[::-1]

ax.plot(save_lengths,savings_gt0,marker='o',color='firebrick',linewidth=0.5,label='>0.0txn')
ax.plot(save_lengths,savings_gt5,marker='8',color='teal',linewidth=0.5,label='>0.5txn')
ax.plot(save_lengths,savings_gt10,marker='h',color='skyblue',linewidth=0.5,label='>1.0txn')
ax.plot(save_lengths,savings_gt25,marker='p',color='gold',linewidth=0.5,label='>2.0txn')

# Percentages on y-axis
ax.set_ylim(0,1.05)
ax.grid(axis='y', color='lightgrey', linestyle='--', linewidth=0.75)
y_vals = ax.get_yticks()
ax.set_yticklabels(['{:,.0%}'.format(x) for x in y_vals])
ax.set_ylabel('Share of accounts', fontsize=12, labelpad=6)

# Days on x-axis
ax.set_axisbelow(True)
ax.grid(axis='x', color='lightgrey', linestyle='--', linewidth=0.75)
ax.set_xticks(['0','7','14','21','28','31+'])
ax.set_xlabel('Days since funds received', fontsize=12, labelpad=6)

# Legend for savings worth
ax.legend(title='Funds remaining:',loc='upper right',bbox_to_anchor=(1.1, 0.87),numpoints=2,borderaxespad=1)

# Save the plot!
plt.savefig(os.path.join(figsdir,'MM_accounts_save_txn.pdf'),dpi=300, bbox_inches='tight')
In [ ]:
! jupyter nbconvert --to html mobile_money.ipynb