Skip to content

Commit 30ca6d6

Browse files
authored
Merge pull request #37 from stellarcarbon/yoy-analytics
YoY analytics
2 parents 8022092 + 3b9496e commit 30ca6d6

File tree

3 files changed

+93
-0
lines changed

3 files changed

+93
-0
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,24 @@ def view_retirements(
231231
) -> pd.DataFrame
232232
```
233233

234+
The `analytics` view provides year-over-year analytics for sinking transactions, aggregating metrics like monthly active users, transaction counts, volume in USD, and carbon sunk, with percentage changes.
235+
236+
```text
237+
Usage: sc-audit view analytics [OPTIONS]
238+
239+
View year-over-year analytics for sinking transactions
240+
241+
Options:
242+
-f, --format [df|csv|json] The output format for this view [default: df]
243+
--help Show this message and exit.
244+
```
245+
246+
The Python API is located at `sc_audit.views.stats` and returns the analytics table as a Pandas DataFrame:
247+
248+
```python
249+
def view_yoy_analytics() -> pd.DataFrame
250+
```
251+
234252
### Configuration
235253

236254
There isn't much to configure in sc-audit. Several settings can be overriden by environment variables.

sc_audit/cli.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from sc_audit.views.inventory import view_inventory
3030
from sc_audit.views.retirement import view_retirements
3131
from sc_audit.views.sink_status import view_sinking_txs
32+
from sc_audit.views.stats import view_yoy_analytics
3233
from sc_audit.views.utils import format_df
3334

3435

@@ -153,6 +154,13 @@ def cli_view_sink_status(
153154
)
154155
click.echo(format_df(txdf, format=format))
155156

157+
158+
@view.command(name="analytics", params=[view_format])
159+
def cli_view_yoy_analytics(format: str):
160+
"""View year-over-year analytics for sinking transactions"""
161+
df = view_yoy_analytics()
162+
click.echo(format_df(df, format=format))
163+
156164
# LOADING
157165

158166
@cli.group()

sc_audit/views/stats.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
"""
22
View Stellarcarbon's asset stats. Optionally, filter by recipient.
3+
Compute year-over-year metrics based on historical sinking transactions.
34
45
Author: Alex Olieman <https://keybase.io/alioli>
56
"""
67

78
from decimal import Decimal
89

10+
import numpy as np
11+
import pandas as pd
912
from sqlalchemy import func, select, union_all
1013

1114
from sc_audit.db_schema.association import SinkStatus
1215
from sc_audit.db_schema.mint import MintedBlock
1316
from sc_audit.db_schema.sink import SinkingTx
1417
from sc_audit.session_manager import Session
18+
from sc_audit.views.sink_status import view_sinking_txs
1519

1620

1721
def get_carbon_stats(recipient: str | None = None) -> dict[str, Decimal]:
@@ -49,3 +53,66 @@ def get_carbon_stats(recipient: str | None = None) -> dict[str, Decimal]:
4953
stats["carbon_pending"] = c_sunk - c_retired
5054

5155
return stats
56+
57+
58+
def view_yoy_analytics() -> pd.DataFrame:
59+
df = view_sinking_txs(order='asc')
60+
if df.empty:
61+
return pd.DataFrame()
62+
63+
# Compute price for USDC transactions
64+
df['price'] = np.where(
65+
df['dest_asset_code'] == 'USDC',
66+
df['dest_asset_amount'].astype(float) / df['carbon_amount'].astype(float),
67+
np.nan
68+
)
69+
70+
# Interpolate prices linearly, then extrapolate using forward fill for trailing CARBON transactions
71+
df['price'] = df['price'].interpolate(method='linear').ffill()
72+
73+
# Convert CARBON denominated amounts to USD using the interpolated price
74+
mask = df['dest_asset_code'] == 'CARBON'
75+
df.loc[mask, 'dest_asset_amount'] = df.loc[mask, 'price'] * df.loc[mask, 'carbon_amount'].astype(float)
76+
77+
# Ensure all dest_asset_amount are float for consistent summing
78+
df['dest_asset_amount'] = df['dest_asset_amount'].astype(float)
79+
80+
# Add year and month columns for binning
81+
df['year'] = df['created_at'].dt.year
82+
df['month'] = df['created_at'].dt.month
83+
84+
# Calculate monthly active users (distinct recipients per month)
85+
mau_df = df.groupby(['year', 'month'])['recipient'].nunique().reset_index()
86+
mau_yearly = (
87+
mau_df.groupby('year')['recipient']
88+
.mean()
89+
.reset_index()
90+
.rename(columns={'recipient': 'mau'})
91+
)
92+
mau_yearly['mau'] = mau_yearly['mau'].astype(float).round(2)
93+
94+
# Calculate yearly aggregates
95+
yearly = df.groupby('year').agg(
96+
num_tx=('hash', 'count'),
97+
volume_usd=('dest_asset_amount', 'sum'),
98+
carbon_sunk=('carbon_amount', 'sum')
99+
).reset_index()
100+
yearly['volume_usd'] = yearly['volume_usd'].astype(float).round(2)
101+
102+
# Merge MAU with yearly data
103+
result = yearly.merge(mau_yearly, on='year', how='left')
104+
105+
# Sort by year
106+
result = result.sort_values('year')
107+
108+
# Calculate year-over-year percentage changes
109+
for col in ['mau', 'num_tx', 'volume_usd', 'carbon_sunk']:
110+
result[f'yoy_{col}'] = (result[col].astype(float).pct_change() * 100).round(2)
111+
112+
# Reorder columns: year, then each core column followed by its yoy column
113+
result = result[[
114+
'year', 'mau', 'yoy_mau', 'num_tx', 'yoy_num_tx',
115+
'volume_usd', 'yoy_volume_usd', 'carbon_sunk', 'yoy_carbon_sunk',
116+
]]
117+
118+
return result

0 commit comments

Comments
 (0)