Files
Stat-Dashboard/mockups/summary_reports.py

111 lines
11 KiB
Python

"""Mockup editorial summaries using the saved report's game and UTC window."""
from collections import defaultdict
from datetime import timedelta
import statistics
def extend_report(conn,report,start,end):
from health_reports import number,change
from sample_api import stage_funnel
gid=report['gameId'];m=report['metrics'];prev=report['previousMetrics']
scope={'gid':gid,'start':start,'end':end}
pct=lambda v: 'N/A' if v is None else number(v)+'%'
minutes=lambda v: 'N/A' if v is None else number(v/60)+' min'
hours=lambda v: 'N/A' if v is None else number(v/3600)+' h'
money=lambda v,decimals=0: 'N/A' if v is None else 'R$ '+number(v,decimals)
timing=lambda v: 'N/A' if v is None else number(v)+' sec'
extra=conn.execute('''SELECT
(SELECT count(DISTINCT player_id) FROM player_active_days WHERE game_id=%(gid)s
AND active_day>=%(end)s::date-30 AND active_day<%(end)s::date)::int mau,
avg(extract(epoch FROM s.ended_at-s.started_at)) FILTER(WHERE (s.started_at AT TIME ZONE 'UTC')::date=(p.first_seen AT TIME ZONE 'UTC')::date)::double precision new_session,
avg(extract(epoch FROM s.ended_at-s.started_at)) FILTER(WHERE (s.started_at AT TIME ZONE 'UTC')::date>(p.first_seen AT TIME ZONE 'UTC')::date)::double precision returning_session
FROM sessions s JOIN players p ON p.game_id=s.game_id AND p.id=s.player_id
WHERE s.game_id=%(gid)s AND s.started_at>=%(start)s::date AND s.started_at<%(end)s::date''',scope).fetchone()
retained={}
for offset in (7,30):
r=conn.execute('''SELECT count(*)::int cohort,count(*) FILTER(WHERE EXISTS(SELECT 1 FROM sessions s
WHERE s.game_id=p.game_id AND s.player_id=p.id AND s.started_at>=(p.first_seen AT TIME ZONE 'UTC')::date+%(offset)s
AND s.started_at<(p.first_seen AT TIME ZONE 'UTC')::date+%(offset)s+1))::int returned
FROM players p WHERE p.game_id=%(gid)s AND p.first_seen>=%(start)s::date-%(offset)s
AND p.first_seen<%(end)s::date-%(offset)s''',scope|{'offset':offset}).fetchone()
retained[offset]=100*r['returned']/r['cohort'] if r['cohort'] else None
products=conn.execute('''SELECT p.id,p.name,sum(s.quantity)::int units,sum(s.unit_price_robux*s.quantity)::double precision robux
FROM product_sales s JOIN products p ON p.game_id=s.game_id AND p.id=s.product_id
WHERE s.game_id=%(gid)s AND s.purchased_at>=%(start)s::date AND s.purchased_at<%(end)s::date
GROUP BY p.id ORDER BY robux DESC,p.id''',scope).fetchall()
payers=conn.execute('''SELECT player_id,sum(unit_price_robux*quantity)::double precision robux FROM product_sales
WHERE game_id=%(gid)s AND purchased_at>=%(start)s::date AND purchased_at<%(end)s::date AND unit_price_robux>0
AND player_id IS NOT NULL GROUP BY player_id ORDER BY robux DESC,player_id''',scope).fetchall()
largest=100*payers[0]['robux']/m['robux'] if payers and m['robux'] and not m['unattributed'] else None
arppu=m['robux']/m['payers'] if m['payers'] and not m['unattributed'] else None
user_days=m['average_dau']*report['periodDays']
arpdau=m['robux']/user_days if user_days else None
prompts=conn.execute('''SELECT trigger_source source,count(*)::int prompts,count(*) FILTER(WHERE outcome='bought')::int bought,
count(*) FILTER(WHERE outcome='closed')::int closed,count(*) FILTER(WHERE outcome IS NULL)::int unresolved,
avg(extract(epoch FROM ended_at-started_at)) FILTER(WHERE outcome='bought')::double precision buy_time,
avg(extract(epoch FROM ended_at-started_at)) FILTER(WHERE outcome='closed')::double precision close_time
FROM interaction_events WHERE game_id=%(gid)s AND kind='product_prompt'
AND outcome IN ('bought','closed') AND ended_at IS NOT NULL AND trigger_source IN ('game','player')
AND started_at>=%(start)s::date AND started_at<%(end)s::date GROUP BY trigger_source''',scope).fetchall()
prompt_by_source={r['source']:dict(r,conversion=100*r['bought']/r['prompts'] if r['prompts'] else None) for r in prompts}
game_prompt=prompt_by_source.get('game',{});player_prompt=prompt_by_source.get('player',{})
definitions=conn.execute('SELECT * FROM activity_definitions WHERE game_id=%s ORDER BY id',(gid,)).fetchall()
runs=conn.execute('''SELECT definition_id,stage_progress,extract(epoch FROM ended_at-started_at)::double precision duration
FROM activity_runs WHERE game_id=%(gid)s AND ended_at IS NOT NULL AND outcome IS NOT NULL AND started_at>=%(start)s::date AND started_at<%(end)s::date''',scope).fetchall()
grouped=defaultdict(list)
for row in runs: grouped[row['definition_id']].append(row)
activity_lines=[];drops=[]
for definition in definitions:
members=grouped[definition['id']]
if not members: continue
times=[r['duration'] for r in members if r['duration'] is not None]
activity_lines.append(f"{definition['name']}: {number(len(members),0)} runs, {minutes(statistics.mean(times) if times else None)} average known duration.")
funnel=stage_funnel(members,definition)
for i,stage in enumerate(funnel[1:],1):
if stage['dropoffPercent'] is not None and funnel[i-1]['runs']:
drops.append({'activity':definition['name'],'from':funnel[i-1]['event'],'to':stage['event'],
'dropoff':stage['dropoffPercent'],'previousRuns':funnel[i-1]['runs'],'reach':stage['reachPercent']})
biggest=max(drops,key=lambda r:r['dropoff']) if drops else None
largest_drop=(f"The largest observed stage loss was in {biggest['activity']}: {pct(biggest['dropoff'])} from {biggest['from']} to {biggest['to']}, among {number(biggest['previousRuns'],0)} runs at the preceding stage. {pct(biggest['reach'])} of all starts reached that stage." if biggest else 'Stage drop-off is N/A because no complete ordered funnel observations are available.')
game=conn.execute('SELECT sample_key FROM games WHERE id=%s',(gid,)).fetchone()
angle={
'willow-v1':'For this social exploration game, inspect whether collection and exploration encourage another visit after the first session.',
'orbit-v1':'For this racing game, inspect how quickly a new player reaches a first race and whether the post-race flow encourages another round.',
'dungeon-v1':'For this progression game, inspect whether early crafting and quest rewards give new players a clear reason to return.'
}.get(game['sample_key'],'Inspect the onboarding and progression steps behind these observed changes.')
audience_grew=m['active_players']>prev['active_players']
revenue_grew=m['robux']>prev['robux']
assessment=('Audience and spending both grew.' if audience_grew and revenue_grew else
'Spending grew while the active audience contracted or stayed flat.' if revenue_grew else
'The audience grew while spending declined or stayed flat.' if audience_grew else
'Active reach and spending were flat or lower than the preceding period.')
top_product=(f"{products[0]['name']} led recorded product spending with {money(products[0]['robux'])} from {number(products[0]['units'],0)} units." if products else 'No product sales were reported in this window.')
prompt_lines=[]
for key,title in [('game','Game-induced'),('player','Player-induced')]:
p=prompt_by_source.get(key)
if not p: prompt_lines.append(f'{title}: no reported prompts.');continue
prompt_lines.append(f"{title}: {number(p['prompts'],0)} prompts, {pct(p['conversion'])} bought; average time to buy {timing(p['buy_time'])}, to close {timing(p['close_time'])}.")
engagement_text=f"{report['gameName']} recorded {number(m['sessions'],0)} session starts and {hours(m['total_playtime'])} of known playtime. Average completed sessions lasted {minutes(m['average_session'])} ({change(m['average_session'],prev['average_session'])} versus the prior period). New-user sessions averaged {minutes(extra['new_session'])}; returning-user sessions averaged {minutes(extra['returning_session'])}."
retention_text=f"Five-minute first-session retention was {pct(m['first_five_rate'])}. Mature exact-day returns were D1 {pct(m['retention'])}, D7 {pct(retained[7])}, and D30 {pct(retained[30])}; these use cohorts whose observation day fell inside this report window."
money_text=f"Reported spending reached {money(m['robux'])} ({change(m['robux'],prev['robux'])}), with {number(m['payers'],0) if not m['unattributed'] else 'N/A'} paying players and {pct(m['payer_conversion'])} period payer conversion. ARPPU was {money(arppu,2)}; Robux per active user-day was {money(arpdau,2)}."
concentration=f"The largest recorded payer contributed {pct(largest)} of period spending. Check whether revenue changes persist without that player before treating them as a broad spending trend." if largest is not None else 'Payer concentration is N/A when no attributed positive-price sales are available.'
investigations=[
{'title':'First-session experience','text':f"Five-minute first-session retention: {pct(m['first_five_rate'])}. {angle}"},
{'title':'Activity transition','text':largest_drop+' Review the preceding step before changing the entire activity.' if biggest else largest_drop},
{'title':'Offer placement','text':f"Player-induced prompts converted at {pct(player_prompt.get('conversion'))}; game-induced prompts at {pct(game_prompt.get('conversion'))}. Compare the same products and similar player groups before testing offer timing or frequency."}
]
sections=[
{'key':'health','title':'Game health','target':'overview','paragraphs':report['paragraphs']},
{'key':'engagement','title':'Engagement & retention','target':'sessions','paragraphs':[{'title':'Session quality','text':engagement_text},{'title':'Returning players','text':retention_text},{'title':'What to inspect','text':angle}]},
{'key':'monetization','title':'Monetization','target':'monetization','paragraphs':[{'title':'Spending & conversion','text':money_text},{'title':'Products','text':top_product},{'title':'Payer concentration','text':concentration}]},
{'key':'prompts','title':'Purchase prompts','target':'prompts','paragraphs':[{'title':'Source breakdown','text':' '.join(prompt_lines)},{'title':'Interpretation','text':'Source comparisons describe different reported audiences, not a proven effect of prompting. Use matching products and player segments to decide which placement to test.'}]},
{'key':'activities','title':'Activities & funnels','target':'activities','paragraphs':[{'title':'Activity use','text':' '.join(activity_lines) or 'No activity runs were reported in this window.'},{'title':'Stage drop-off','text':largest_drop}]},
{'key':'investigations','title':'Suggested investigations','target':None,'paragraphs':investigations}
]
evidence={'mau':extra['mau'],'d7':retained[7],'d30':retained[30],'arppu':arppu,'arpdau':arpdau,
'newSession':extra['new_session'],'returningSession':extra['returning_session'],
'largestPayerShare':largest,'topProducts':products,'promptSources':prompt_by_source,'largestDrop':biggest}
from overview_analysis import deep_analysis
executive=deep_analysis(conn,report,start,end,evidence)
return {'summaryVersion':4,'executive':executive,'summaries':sections,'summaryEvidence':evidence}