413 lines
17 KiB
Python
413 lines
17 KiB
Python
from django.core.management.base import BaseCommand
|
|
from django.utils import timezone
|
|
from django.db import transaction
|
|
from django.db.models import Avg
|
|
from datetime import timedelta, datetime
|
|
import random
|
|
from tqdm import tqdm
|
|
|
|
from apps.surveys.models import (
|
|
SurveyTemplate, SurveyQuestion, SurveyInstance,
|
|
SurveyResponse, SurveyTracking, SurveyStatus
|
|
)
|
|
from apps.organizations.models import Hospital, Patient
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = 'Seed historical survey data for analytics (1 year timeframe)'
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
'--months',
|
|
type=int,
|
|
default=12,
|
|
help='Number of months of historical data to generate (default: 12)'
|
|
)
|
|
parser.add_argument(
|
|
'--surveys-per-month',
|
|
type=int,
|
|
default=300,
|
|
help='Number of surveys to generate per month (default: 300)'
|
|
)
|
|
parser.add_argument(
|
|
'--clear',
|
|
action='store_true',
|
|
help='Clear existing survey instances before seeding'
|
|
)
|
|
|
|
def handle(self, *args, **options):
|
|
months = options['months']
|
|
surveys_per_month = options['surveys_per_month']
|
|
clear = options['clear']
|
|
|
|
self.stdout.write(self.style.SUCCESS('='*70))
|
|
self.stdout.write(self.style.SUCCESS('SEEDING HISTORICAL SURVEY DATA'))
|
|
self.stdout.write(self.style.SUCCESS('='*70))
|
|
|
|
# Get hospital
|
|
hospital = Hospital.objects.filter(status='active').first()
|
|
if not hospital:
|
|
self.stdout.write(self.style.ERROR('No active hospital found!'))
|
|
return
|
|
|
|
self.stdout.write(f'Using hospital: {hospital.name}')
|
|
|
|
# Clear existing data if requested
|
|
if clear:
|
|
self.stdout.write('\nClearing existing survey instances...')
|
|
deleted_count = SurveyInstance.objects.all().delete()[0]
|
|
self.stdout.write(f'✓ Deleted {deleted_count} survey instances')
|
|
|
|
# Get or create survey templates
|
|
self.stdout.write('\nChecking survey templates...')
|
|
templates = self.get_or_create_templates(hospital)
|
|
self.stdout.write(f'✓ Found/created {len(templates)} survey templates')
|
|
|
|
# Get patients
|
|
patients = list(Patient.objects.all())
|
|
if not patients:
|
|
self.stdout.write(self.style.ERROR('No patients found! Please create patients first.'))
|
|
return
|
|
|
|
self.stdout.write(f'✓ Found {len(patients)} patients')
|
|
|
|
# Generate surveys
|
|
total_surveys = months * surveys_per_month
|
|
self.stdout.write(f'\nGenerating {total_surveys} surveys over {months} months...\n')
|
|
|
|
with transaction.atomic():
|
|
# Generate surveys month by month
|
|
current_date = timezone.now()
|
|
all_surveys = []
|
|
|
|
for month_offset in tqdm(range(months), desc="Generating surveys", unit="month"):
|
|
month_date = current_date - timedelta(days=month_offset * 30)
|
|
surveys_for_month = self.generate_monthly_surveys(
|
|
templates, patients, hospital, month_date, surveys_per_month
|
|
)
|
|
all_surveys.extend(surveys_for_month)
|
|
|
|
self.stdout.write(f'\n Generated {len(surveys_for_month)} surveys for month {month_offset + 1}')
|
|
|
|
# Display summary
|
|
self.stdout.write('\n' + '='*70)
|
|
self.stdout.write(self.style.SUCCESS('GENERATION COMPLETE'))
|
|
self.stdout.write('='*70)
|
|
|
|
# Statistics
|
|
completed = SurveyInstance.objects.filter(status='completed').count()
|
|
negative = SurveyInstance.objects.filter(is_negative=True).count()
|
|
with_comments = SurveyInstance.objects.exclude(comment='').count()
|
|
|
|
# Score distribution
|
|
self.stdout.write(f'\nTotal Surveys: {SurveyInstance.objects.count()}')
|
|
self.stdout.write(f'Completed: {completed} ({completed/total_surveys*100:.1f}%)')
|
|
self.stdout.write(f'Negative: {negative} ({negative/completed*100 if completed > 0 else 0:.1f}% of completed)')
|
|
self.stdout.write(f'With Comments: {with_comments}')
|
|
|
|
# By template
|
|
self.stdout.write('\nBy Survey Template:')
|
|
for template in templates:
|
|
count = SurveyInstance.objects.filter(survey_template=template).count()
|
|
avg_score = SurveyInstance.objects.filter(
|
|
survey_template=template,
|
|
total_score__isnull=False
|
|
).aggregate(avg=Avg('total_score'))['avg']
|
|
avg_score_display = f'{avg_score:.2f}' if avg_score else 'N/A'
|
|
self.stdout.write(
|
|
f' {template.name}: {count} surveys '
|
|
f'(avg score: {avg_score_display})'
|
|
)
|
|
|
|
self.stdout.write(self.style.SUCCESS('\n✓ Historical survey data seeded successfully!'))
|
|
|
|
def get_or_create_templates(self, hospital):
|
|
"""Get or create survey templates for all patient types"""
|
|
|
|
template_definitions = [
|
|
{
|
|
'name': 'Inpatient Post-Discharge Survey',
|
|
'survey_type': 'stage',
|
|
'questions': [
|
|
'How satisfied were you with the nursing care?',
|
|
'How satisfied were you with the doctor\'s care?',
|
|
'How clean was your room?',
|
|
'How satisfied were you with the food quality?',
|
|
'How well were you informed about your treatment?',
|
|
'How likely are you to recommend this hospital to others?'
|
|
]
|
|
},
|
|
{
|
|
'name': 'OPD Patient Experience Survey',
|
|
'survey_type': 'stage',
|
|
'questions': [
|
|
'How satisfied were you with the registration process?',
|
|
'How satisfied were you with the waiting time?',
|
|
'How satisfied were you with the doctor\'s consultation?',
|
|
'How satisfied were you with the pharmacy service?',
|
|
'How likely are you to recommend this hospital to others?'
|
|
]
|
|
},
|
|
{
|
|
'name': 'EMS Emergency Services Survey',
|
|
'survey_type': 'stage',
|
|
'questions': [
|
|
'How satisfied were you with the ambulance response time?',
|
|
'How satisfied were you with the paramedic care?',
|
|
'How satisfied were you with the emergency department care?',
|
|
'How satisfied were you with the communication from staff?',
|
|
'How likely are you to recommend this hospital to others?'
|
|
]
|
|
},
|
|
{
|
|
'name': 'Day Case Patient Survey',
|
|
'survey_type': 'stage',
|
|
'questions': [
|
|
'How satisfied were you with the pre-procedure preparation?',
|
|
'How satisfied were you with the procedure itself?',
|
|
'How satisfied were you with the post-procedure care?',
|
|
'How satisfied were you with the discharge process?',
|
|
'How likely are you to recommend this hospital to others?'
|
|
]
|
|
}
|
|
]
|
|
|
|
templates = []
|
|
|
|
for template_def in template_definitions:
|
|
template, created = SurveyTemplate.objects.get_or_create(
|
|
name=template_def['name'],
|
|
hospital=hospital,
|
|
defaults={
|
|
'survey_type': template_def['survey_type'],
|
|
'is_active': True
|
|
}
|
|
)
|
|
|
|
# Create questions if template was just created
|
|
if created:
|
|
for order, question_text in enumerate(template_def['questions'], start=1):
|
|
question_type = 'nps' if 'recommend' in question_text.lower() else 'rating'
|
|
SurveyQuestion.objects.create(
|
|
survey_template=template,
|
|
text=question_text,
|
|
question_type=question_type,
|
|
order=order,
|
|
is_required=True
|
|
)
|
|
|
|
templates.append(template)
|
|
|
|
return templates
|
|
|
|
def generate_monthly_surveys(self, templates, patients, hospital, month_date, count):
|
|
"""Generate surveys for a specific month"""
|
|
surveys = []
|
|
|
|
# Distribute surveys across the month
|
|
for i in range(count):
|
|
# Random day within the month
|
|
day_offset = random.randint(0, 29)
|
|
survey_date = month_date + timedelta(days=day_offset)
|
|
|
|
# Select random template and patient
|
|
template = random.choice(templates)
|
|
patient = random.choice(patients)
|
|
|
|
# Determine status (weighted distribution)
|
|
status_roll = random.random()
|
|
if status_roll < 0.85:
|
|
status = SurveyStatus.COMPLETED
|
|
elif status_roll < 0.95:
|
|
status = SurveyStatus.ABANDONED
|
|
elif status_roll < 0.98:
|
|
status = SurveyStatus.IN_PROGRESS
|
|
else:
|
|
status = SurveyStatus.VIEWED
|
|
|
|
# Create survey instance
|
|
survey = SurveyInstance.objects.create(
|
|
survey_template=template,
|
|
patient=patient,
|
|
hospital=hospital,
|
|
status=status,
|
|
sent_at=survey_date,
|
|
delivery_channel=random.choice(['sms', 'whatsapp', 'email']),
|
|
recipient_phone=patient.phone,
|
|
recipient_email=patient.email,
|
|
)
|
|
|
|
# Set timestamps based on status
|
|
if status == SurveyStatus.COMPLETED:
|
|
survey.opened_at = survey.sent_at + timedelta(minutes=random.randint(5, 60))
|
|
survey.completed_at = survey.opened_at + timedelta(minutes=random.randint(2, 15))
|
|
survey.time_spent_seconds = random.randint(60, 900)
|
|
survey.open_count = random.randint(1, 3)
|
|
survey.last_opened_at = survey.completed_at
|
|
elif status == SurveyStatus.ABANDONED:
|
|
survey.opened_at = survey.sent_at + timedelta(minutes=random.randint(5, 60))
|
|
survey.time_spent_seconds = random.randint(30, 300)
|
|
survey.open_count = 1
|
|
survey.last_opened_at = survey.opened_at
|
|
elif status == SurveyStatus.IN_PROGRESS:
|
|
survey.opened_at = survey.sent_at + timedelta(minutes=random.randint(5, 60))
|
|
survey.open_count = random.randint(1, 2)
|
|
survey.last_opened_at = survey.opened_at
|
|
elif status == SurveyStatus.VIEWED:
|
|
survey.opened_at = survey.sent_at + timedelta(minutes=random.randint(5, 60))
|
|
survey.open_count = 1
|
|
survey.last_opened_at = survey.opened_at
|
|
|
|
survey.save()
|
|
|
|
# Generate responses for completed surveys
|
|
if status == SurveyStatus.COMPLETED:
|
|
self.generate_survey_responses(survey, template)
|
|
|
|
# Add comment for some surveys (more common for negative ones)
|
|
comment_roll = random.random()
|
|
should_add_comment = (
|
|
comment_roll < 0.25 if survey.is_negative else comment_roll < 0.15
|
|
)
|
|
|
|
if should_add_comment:
|
|
survey.comment = self.generate_comment(survey.is_negative)
|
|
survey.save(update_fields=['comment'])
|
|
|
|
# Create tracking events
|
|
self.create_tracking_events(survey)
|
|
|
|
surveys.append(survey)
|
|
|
|
return surveys
|
|
|
|
def generate_survey_responses(self, survey, template):
|
|
"""Generate responses for a survey instance"""
|
|
questions = template.questions.all()
|
|
|
|
# Determine target score range (weighted distribution)
|
|
score_roll = random.random()
|
|
if score_roll < 0.35:
|
|
# Excellent (4.5-5.0)
|
|
min_score, max_score = 4.5, 5.0
|
|
elif score_roll < 0.65:
|
|
# Good (4.0-4.4)
|
|
min_score, max_score = 4.0, 4.4
|
|
elif score_roll < 0.85:
|
|
# Average (3.5-3.9)
|
|
min_score, max_score = 3.5, 3.9
|
|
elif score_roll < 0.95:
|
|
# Poor (3.0-3.4)
|
|
min_score, max_score = 3.0, 3.4
|
|
else:
|
|
# Negative (below 3.0)
|
|
min_score, max_score = 1.5, 2.9
|
|
|
|
# Generate responses for each question
|
|
for question in questions:
|
|
# Generate score within target range
|
|
if question.question_type == 'rating':
|
|
score = round(random.uniform(min_score, max_score), 1)
|
|
SurveyResponse.objects.create(
|
|
survey_instance=survey,
|
|
question=question,
|
|
numeric_value=score
|
|
)
|
|
elif question.question_type == 'nps':
|
|
# NPS: 0-10 scale
|
|
if min_score >= 4.5:
|
|
nps_score = random.randint(9, 10) # Promoters
|
|
elif min_score >= 4.0:
|
|
nps_score = random.randint(7, 8) # Passive
|
|
elif min_score >= 3.5:
|
|
nps_score = random.randint(5, 6) # Passive
|
|
elif min_score >= 3.0:
|
|
nps_score = random.randint(3, 4) # Detractors
|
|
else:
|
|
nps_score = random.randint(0, 2) # Detractors
|
|
|
|
SurveyResponse.objects.create(
|
|
survey_instance=survey,
|
|
question=question,
|
|
numeric_value=nps_score
|
|
)
|
|
|
|
# Calculate score
|
|
score = survey.calculate_score()
|
|
|
|
def create_tracking_events(self, survey):
|
|
"""Create tracking events for a survey"""
|
|
# Page view
|
|
SurveyTracking.objects.create(
|
|
survey_instance=survey,
|
|
event_type='page_view',
|
|
user_agent='Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)',
|
|
ip_address='192.168.1.' + str(random.randint(1, 254)),
|
|
device_type=random.choice(['mobile', 'mobile', 'desktop']),
|
|
browser='Safari 14.0',
|
|
created_at=survey.sent_at
|
|
)
|
|
|
|
# Survey started
|
|
if survey.opened_at:
|
|
SurveyTracking.objects.create(
|
|
survey_instance=survey,
|
|
event_type='survey_started',
|
|
user_agent='Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)',
|
|
ip_address='192.168.1.' + str(random.randint(1, 254)),
|
|
device_type=random.choice(['mobile', 'mobile', 'desktop']),
|
|
browser='Safari 14.0',
|
|
created_at=survey.opened_at,
|
|
metadata={'referrer': 'https://wa.me/'}
|
|
)
|
|
|
|
# Survey completed
|
|
if survey.completed_at:
|
|
SurveyTracking.objects.create(
|
|
survey_instance=survey,
|
|
event_type='survey_completed',
|
|
user_agent='Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)',
|
|
ip_address='192.168.1.' + str(random.randint(1, 254)),
|
|
device_type=random.choice(['mobile', 'mobile', 'desktop']),
|
|
browser='Safari 14.0',
|
|
created_at=survey.completed_at,
|
|
total_time_spent=survey.time_spent_seconds,
|
|
metadata={
|
|
'response_count': survey.survey_template.questions.count()
|
|
}
|
|
)
|
|
|
|
def generate_comment(self, is_negative):
|
|
"""Generate a realistic comment based on sentiment"""
|
|
|
|
positive_comments = [
|
|
"Excellent service, very satisfied with everything!",
|
|
"Staff was very professional and caring. Great experience.",
|
|
"Clean facilities and friendly staff. Highly recommend.",
|
|
"The doctors and nurses were amazing. Thank you!",
|
|
"Very good care, would definitely come back.",
|
|
"Professional service and attentive staff.",
|
|
"Everything was perfect, no complaints at all.",
|
|
"Best hospital experience I've ever had.",
|
|
"Staff went above and beyond to help me.",
|
|
"Quick service and excellent medical care."
|
|
]
|
|
|
|
negative_comments = [
|
|
"The waiting time was too long, very frustrating.",
|
|
"Staff seemed rushed and didn't explain things well.",
|
|
"Facilities were not clean enough, needs improvement.",
|
|
"Communication with staff was poor and confusing.",
|
|
"Had to wait too long for test results.",
|
|
"The staff was not very friendly or helpful.",
|
|
"Overall experience was disappointing, needs work.",
|
|
"Not satisfied with the level of care provided.",
|
|
"The process was disorganized and slow.",
|
|
"Expected better service and attention to detail."
|
|
]
|
|
|
|
if is_negative:
|
|
return random.choice(negative_comments)
|
|
else:
|
|
return random.choice(positive_comments)
|