Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,24 @@ def view_retirements(
) -> pd.DataFrame
```

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.

```text
Usage: sc-audit view analytics [OPTIONS]

View year-over-year analytics for sinking transactions

Options:
-f, --format [df|csv|json] The output format for this view [default: df]
--help Show this message and exit.
```

The Python API is located at `sc_audit.views.stats` and returns the analytics table as a Pandas DataFrame:

```python
def view_yoy_analytics() -> pd.DataFrame
```

### Configuration

There isn't much to configure in sc-audit. Several settings can be overriden by environment variables.
Expand Down
8 changes: 8 additions & 0 deletions sc_audit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from sc_audit.views.inventory import view_inventory
from sc_audit.views.retirement import view_retirements
from sc_audit.views.sink_status import view_sinking_txs
from sc_audit.views.stats import view_yoy_analytics
from sc_audit.views.utils import format_df


Expand Down Expand Up @@ -153,6 +154,13 @@ def cli_view_sink_status(
)
click.echo(format_df(txdf, format=format))


@view.command(name="analytics", params=[view_format])
def cli_view_yoy_analytics(format: str):
"""View year-over-year analytics for sinking transactions"""
df = view_yoy_analytics()
click.echo(format_df(df, format=format))

# LOADING

@cli.group()
Expand Down
67 changes: 67 additions & 0 deletions sc_audit/views/stats.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
"""
View Stellarcarbon's asset stats. Optionally, filter by recipient.
Compute year-over-year metrics based on historical sinking transactions.

Author: Alex Olieman <https://keybase.io/alioli>
"""

from decimal import Decimal

import numpy as np
import pandas as pd
from sqlalchemy import func, select, union_all

from sc_audit.db_schema.association import SinkStatus
from sc_audit.db_schema.mint import MintedBlock
from sc_audit.db_schema.sink import SinkingTx
from sc_audit.session_manager import Session
from sc_audit.views.sink_status import view_sinking_txs


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

return stats


def view_yoy_analytics() -> pd.DataFrame:
df = view_sinking_txs(order='asc')
if df.empty:
return pd.DataFrame()

# Compute price for USDC transactions
df['price'] = np.where(
df['dest_asset_code'] == 'USDC',
df['dest_asset_amount'].astype(float) / df['carbon_amount'].astype(float),
np.nan
)

# Interpolate prices linearly, then extrapolate using forward fill for trailing CARBON transactions
df['price'] = df['price'].interpolate(method='linear').ffill()

# Convert CARBON denominated amounts to USD using the interpolated price
mask = df['dest_asset_code'] == 'CARBON'
df.loc[mask, 'dest_asset_amount'] = df.loc[mask, 'price'] * df.loc[mask, 'carbon_amount'].astype(float)

# Ensure all dest_asset_amount are float for consistent summing
df['dest_asset_amount'] = df['dest_asset_amount'].astype(float)

# Add year and month columns for binning
df['year'] = df['created_at'].dt.year
df['month'] = df['created_at'].dt.month

# Calculate monthly active users (distinct recipients per month)
mau_df = df.groupby(['year', 'month'])['recipient'].nunique().reset_index()
mau_yearly = (
mau_df.groupby('year')['recipient']
.mean()
.reset_index()
.rename(columns={'recipient': 'mau'})
)
mau_yearly['mau'] = mau_yearly['mau'].astype(float).round(2)

# Calculate yearly aggregates
yearly = df.groupby('year').agg(
num_tx=('hash', 'count'),
volume_usd=('dest_asset_amount', 'sum'),
carbon_sunk=('carbon_amount', 'sum')
).reset_index()
yearly['volume_usd'] = yearly['volume_usd'].astype(float).round(2)

# Merge MAU with yearly data
result = yearly.merge(mau_yearly, on='year', how='left')

# Sort by year
result = result.sort_values('year')

# Calculate year-over-year percentage changes
for col in ['mau', 'num_tx', 'volume_usd', 'carbon_sunk']:
result[f'yoy_{col}'] = (result[col].astype(float).pct_change() * 100).round(2)

# Reorder columns: year, then each core column followed by its yoy column
result = result[[
'year', 'mau', 'yoy_mau', 'num_tx', 'yoy_num_tx',
'volume_usd', 'yoy_volume_usd', 'carbon_sunk', 'yoy_carbon_sunk',
]]

return result