Spaces:
Sleeping
Sleeping
File size: 5,462 Bytes
7a30d73 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | from datetime import datetime, timedelta
from collections import defaultdict
import numpy as np
from sqlalchemy.orm import Session
from app.database.models import Account, Transaction
def analyze_spending_behavior(db: Session, user_id: str, days: int = 90):
"""
Analyzes historical transactions to detect behavioral patterns (late-night, impulsive, dopamine, stress).
"""
accounts = db.query(Account).filter(Account.user_id == user_id).all()
account_ids = [acc.id for acc in accounts]
if not account_ids:
return {"insights": [], "metrics": {}}
cutoff = datetime.utcnow() - timedelta(days=days)
txns = db.query(Transaction).filter(
Transaction.account_id.in_(account_ids),
Transaction.timestamp >= cutoff,
Transaction.type == "debit"
).all()
if not txns:
return {
"insights": ["No recent debit transactions to analyze. Complete a few purchases to start behavioral profiling."],
"metrics": {
"late_night_count": 0,
"late_night_total": 0.0,
"weekend_pct": 0.0,
"impulsive_count": 0,
"impulsive_total": 0.0,
"dopamine_count": 0,
"stress_count": 0
}
}
# Analyze variables
late_night_txns = []
weekend_txns = []
impulsive_txns = []
dopamine_txns = []
stress_txns = []
amounts = [t.amount for t in txns]
avg_txn = np.mean(amounts)
std_txn = np.std(amounts) if len(amounts) > 1 else 0.0
category_totals = defaultdict(float)
hourly_counts = defaultdict(int)
for t in txns:
# Categorize
category_totals[t.category or "Other"] += t.amount
# Timing
hour = t.timestamp.hour
hourly_counts[hour] += 1
# Late-night spending (11PM to 4AM)
if hour >= 23 or hour < 4:
late_night_txns.append(t)
# Weekend spending (Friday, Saturday, Sunday)
# weekday() is 0=Monday, 4=Friday, 5=Saturday, 6=Sunday
day = t.timestamp.weekday()
if day in [4, 5, 6]:
weekend_txns.append(t)
# Impulsive spending (More than average + 1.5 * standard dev, or marked as 'regret')
if t.amount > (avg_txn + 1.5 * std_txn) and (t.category in ["Shopping", "Entertainment", "Food"]):
impulsive_txns.append(t)
# Emotion tags
emotion = (t.spending_emotion_label or "").lower()
if emotion == "regret":
stress_txns.append(t)
elif emotion in ["happy", "dopamine"] or (t.category == "Shopping" and t.amount > avg_txn):
dopamine_txns.append(t)
# Insights construction
insights = []
# 1. Late night alert
late_night_pct = (len(late_night_txns) / len(txns) * 100) if txns else 0
if late_night_pct > 15:
total_late = sum(t.amount for t in late_night_txns)
insights.append(
f"๐ High late-night spending: {late_night_pct:.1f}% of transactions occur after 11PM (Total: ${total_late:,.2f}). "
"Consider setting a bedtime blocker on your bank card."
)
# 2. Weekend overspending
weekend_pct = (len(weekend_txns) / len(txns) * 100) if txns else 0
if weekend_pct > 45:
weekend_avg = np.mean([t.amount for t in weekend_txns]) if weekend_txns else 0
weekday_txns = [t for t in txns if t not in weekend_txns]
weekday_avg = np.mean([t.amount for t in weekday_txns]) if weekday_txns else 0
if weekend_avg > weekday_avg * 1.2:
pct_diff = ((weekend_avg - weekday_avg) / weekday_avg) * 100
insights.append(
f"๐ Weekend Spikes: You spend {pct_diff:.1f}% more on weekends than weekdays. "
"Mainly driven by dining out and recreational purchases."
)
# 3. Dopamine triggers
if len(dopamine_txns) > 3:
insights.append(
f"๐๏ธ Dopamine Spending: Detected {len(dopamine_txns)} shopping spikes. "
"These purchases often occur in bursts, indicating reward-seeking behavior."
)
# 4. Stress/Regret Spending
if len(stress_txns) > 0:
insights.append(
f"โ ๏ธ Emotional Spending: You flagged {len(stress_txns)} transactions as 'regret' or 'stress spending'. "
"Implementing a 24-hour cooling-off rule for non-essential items over $100 could help."
)
# General fallback if no major insights
if not insights:
insights.append("๐ Spending Discipline: Your transactions exhibit stable and regular timing, with minimal signs of emotional or impulsive spending.")
return {
"insights": insights,
"metrics": {
"late_night_count": len(late_night_txns),
"late_night_total": round(sum(t.amount for t in late_night_txns), 2),
"weekend_pct": round(weekend_pct, 2),
"impulsive_count": len(impulsive_txns),
"impulsive_total": round(sum(t.amount for t in impulsive_txns), 2),
"dopamine_count": len(dopamine_txns),
"stress_count": len(stress_txns),
"avg_transaction_amount": round(avg_txn, 2)
},
"category_breakdown": {cat: round(amt, 2) for cat, amt in category_totals.items()}
}
|