Skip to content

Commit fd1edff

Browse files
committed
Add reporting utils
This adds a bunch of reporting utilities. This is used for displaying subscriber information and to dump active subscriber data for damus wrapped (see Makefile) Cc: Daniel Daquino <daniel@daquino.me> Signed-off-by: William Casarin <jb55@jb55.com>
1 parent 4d6add6 commit fd1edff

File tree

3 files changed

+208
-1
lines changed

3 files changed

+208
-1
lines changed

Makefile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
tags: fake
3+
find src test -name '*.js' | xargs ctags
4+
5+
data.json: fake
6+
rsync -avzP purple:api/ data/
7+
node scripts/dump.js data/production > data.json
8+
9+
accounts.json: data.json
10+
jq '.accounts|to_entries' data.json > accounts.json
11+
12+
subscriptions.json: data.json
13+
./scripts/report.py subs_report
14+
15+
active-subscriptions.json: subscriptions.json
16+
jq --argjson now "$(shell date +%s)" 'map(select($$now >= .start_date and $$now <= .end_date))' < $^ > $@
17+
18+
active-pubkeys.json: active-subscriptions.json
19+
jq 'map(.pubkey) | sort | unique' $^ > $@
20+
21+
report: accounts.json
22+
./scripts/report.py
23+
24+
.PHONY: fake

scripts/report.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env python3
2+
import json
3+
import pandas as pd
4+
import plotly.express as px
5+
import matplotlib.pyplot as plt
6+
import sys
7+
from datetime import datetime
8+
9+
10+
def dogant(data):
11+
# Convert data to DataFrame
12+
df = pd.DataFrame(data)
13+
14+
# Convert Unix timestamps to readable dates
15+
df['start_date'] = pd.to_datetime(df['start_date'], unit='s')
16+
df['end_date'] = pd.to_datetime(df['end_date'], unit='s')
17+
18+
# Create a Gantt chart
19+
fig = px.timeline(df, x_start="start_date", x_end="end_date", y="user_id", color="type", title="Subscription Periods")
20+
fig.update_yaxes(title="User ID")
21+
fig.update_xaxes(title="Date")
22+
fig.update_layout(showlegend=True)
23+
24+
# Current date
25+
current_date = datetime.now()
26+
27+
# Add a vertical line for the current date
28+
fig.add_shape(
29+
dict(
30+
type="line",
31+
x0=current_date,
32+
y0=0,
33+
x1=current_date,
34+
y1=1,
35+
xref='x',
36+
yref='paper',
37+
line=dict(color="Red", width=2)
38+
)
39+
)
40+
41+
# Update layout for the shape
42+
fig.update_layout(
43+
shapes=[
44+
dict(
45+
type="line",
46+
x0=current_date,
47+
x1=current_date,
48+
y0=0,
49+
y1=1,
50+
xref='x',
51+
yref='paper',
52+
line=dict(color="Purple", width=1)
53+
)
54+
]
55+
)
56+
57+
fig.show()
58+
59+
def calc_subs(data):
60+
# Convert JSON data to DataFrame
61+
subscriptions = []
62+
max_date = datetime(2100, 1, 1)
63+
64+
for entry in data:
65+
if not 'transactions' in entry['value']:
66+
continue
67+
68+
transactions = entry['value']['transactions']
69+
pubkey = entry['value']['pubkey']
70+
for transaction in transactions:
71+
start_date = transaction['start_date']
72+
end_date = transaction['end_date']
73+
purchased_date = transaction['purchased_date']
74+
duration = transaction['duration']
75+
76+
if start_date is None:
77+
start_date = purchased_date
78+
79+
if end_date is None and duration is not None:
80+
end_date = start_date + duration
81+
82+
transaction['start_date'] = start_date
83+
transaction['end_date'] = end_date
84+
85+
transaction.pop('duration', None)
86+
transaction.pop('purchased_date', None)
87+
88+
if end_date > (4102473600 * 2):
89+
print("{} has bad end date".format(transaction), file=sys.stderr)
90+
continue
91+
92+
if start_date is not None and end_date is not None:
93+
subscriptions.append({
94+
'start_date': start_date,
95+
'end_date': end_date,
96+
'type': transaction['type'],
97+
'user_id': int(entry['key']),
98+
'pubkey': pubkey
99+
})
100+
else:
101+
print("missing start or end date in {}".format(transaction), file=sys.stderr)
102+
103+
return subscriptions
104+
105+
106+
def load_data():
107+
with open('accounts.json', 'r') as file:
108+
return json.load(file)
109+
110+
def active_subs_plot(data):
111+
# Convert data to DataFrame
112+
df = pd.DataFrame(data)
113+
114+
# Convert Unix timestamps to datetime
115+
df['start_date'] = pd.to_datetime(df['start_date'], unit='s')
116+
df['end_date'] = pd.to_datetime(df['end_date'], unit='s')
117+
118+
# Define the end of the next year
119+
end_of_year = pd.Timestamp.now() + pd.DateOffset(months=2)
120+
121+
# Create a date range for each subscription
122+
df['date_range'] = df.apply(lambda row: pd.date_range(start=row['start_date'], end=row['end_date'], freq='D'), axis=1)
123+
124+
# Explode the date_range to get a row for each day a subscription is active
125+
df = df.explode('date_range')
126+
127+
# Ensure date_range is datetime type
128+
df['date_range'] = pd.to_datetime(df['date_range'])
129+
130+
# Filter out dates beyond the end of the next year
131+
df = df[df['date_range'] <= end_of_year]
132+
133+
# Count active subscriptions per month and type
134+
df['month'] = df['date_range'].dt.to_period('M')
135+
monthly_active_subscriptions = df.groupby(['month', 'type'])['user_id'].nunique().unstack().fillna(0)
136+
137+
colors = {'iap': '#00CC96', 'ln': '#EF553B'}
138+
139+
# Plotting
140+
plt.figure(figsize=(14, 8))
141+
monthly_active_subscriptions.plot(kind='bar', stacked=True, color=[colors.get(x, '#636EFA') for x in monthly_active_subscriptions.columns], ax=plt.gca())
142+
plt.title('Monthly Active Subscriptions by Type')
143+
plt.xlabel('Month')
144+
plt.ylabel('Number of Active Subscriptions')
145+
plt.xticks(rotation=45)
146+
plt.legend(title='Subscription Type')
147+
plt.tight_layout()
148+
plt.show()
149+
150+
151+
def write_subs(subs):
152+
with open('subscriptions.json', 'w') as file:
153+
json.dump(subs, file)
154+
155+
def print_data():
156+
data = load_data()
157+
subs = calc_subs(data)
158+
print(json.dumps(subs))
159+
160+
def report():
161+
# Example JSON data (you can replace this with loading your actual JSON data)
162+
data = load_data()
163+
subs = calc_subs(data)
164+
165+
active_subs_plot(subs)
166+
dogant(subs)
167+
168+
write_subs(subs)
169+
170+
def subs_report():
171+
data = load_data()
172+
subs = calc_subs(data)
173+
write_subs(subs)
174+
175+
if len(sys.argv) > 1:
176+
func_name = sys.argv[1]
177+
if func_name in globals() and callable(globals()[func_name]):
178+
globals()[func_name]()
179+
else:
180+
print(f"Error: Unknown command '{func_name}'.")
181+
else:
182+
report()
183+

shell.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{ pkgs ? import <nixpkgs> {} }:
22
with pkgs;
33
mkShell {
4-
buildInputs = [ node2nix ];
4+
buildInputs = [ node2nix ] ++ (with python3Packages; [ pandas matplotlib plotly ]);
55
}

0 commit comments

Comments
 (0)