saas-market-analysis-dubai/apps/analytics/views.py

662 lines
24 KiB
Python

"""
Views for analytics and data analysis.
"""
from rest_framework import generics, status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from django.db.models import Avg, Count, Min, Max, Sum, Q
from django.utils import timezone
from datetime import datetime, timedelta
from .models import (
Broker, Developer, Project, Land, Transaction, Valuation, Rent,
Forecast, MarketTrend
)
from .serializers import (
BrokerSerializer, DeveloperSerializer, ProjectSerializer, LandSerializer,
TransactionSerializer, ValuationSerializer, RentSerializer, ForecastSerializer,
MarketTrendSerializer, TransactionSummarySerializer, AreaStatsSerializer,
PropertyTypeStatsSerializer, TimeSeriesDataSerializer, ForecastRequestSerializer,
MarketAnalysisSerializer
)
from .services import AnalyticsService, ForecastingService
import logging
logger = logging.getLogger(__name__)
class TransactionListView(generics.ListAPIView):
"""List transactions with filtering and pagination."""
serializer_class = TransactionSerializer
permission_classes = [IsAuthenticated]
filterset_fields = ['area_en', 'property_type', 'property_sub_type', 'group', 'usage']
search_fields = ['area_en', 'property_type', 'nearest_metro', 'nearest_mall']
ordering_fields = ['instance_date', 'transaction_value', 'actual_area']
ordering = ['-instance_date']
def get_queryset(self):
queryset = Transaction.objects.all()
# Date range filtering
start_date = self.request.query_params.get('start_date')
end_date = self.request.query_params.get('end_date')
if start_date:
queryset = queryset.filter(instance_date__gte=start_date)
if end_date:
queryset = queryset.filter(instance_date__lte=end_date)
# Price range filtering
min_price = self.request.query_params.get('min_price')
max_price = self.request.query_params.get('max_price')
if min_price:
queryset = queryset.filter(transaction_value__gte=min_price)
if max_price:
queryset = queryset.filter(transaction_value__lte=max_price)
# Area filtering
area = self.request.query_params.get('area')
if area:
queryset = queryset.filter(area_en__icontains=area)
return queryset
class ProjectListView(generics.ListAPIView):
"""List projects with filtering."""
serializer_class = ProjectSerializer
permission_classes = [IsAuthenticated]
filterset_fields = ['project_status', 'area_en', 'zone_en', 'developer']
search_fields = ['project_name_en', 'area_en', 'zone_en']
ordering_fields = ['start_date', 'project_value', 'percent_completed']
ordering = ['-start_date']
def get_queryset(self):
return Project.objects.select_related('developer').all()
class BrokerListView(generics.ListAPIView):
"""List brokers with filtering."""
serializer_class = BrokerSerializer
permission_classes = [IsAuthenticated]
filterset_fields = ['real_estate_name_en']
search_fields = ['broker_name_en', 'real_estate_name_en']
ordering_fields = ['broker_name_en', 'license_end_date']
ordering = ['broker_name_en']
def get_queryset(self):
return Broker.objects.all()
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def transaction_summary(request):
"""Get transaction summary statistics."""
try:
# Get query parameters
area = request.query_params.get('area')
property_type = request.query_params.get('property_type')
start_date = request.query_params.get('start_date')
end_date = request.query_params.get('end_date')
# Build queryset
queryset = Transaction.objects.all()
if area:
queryset = queryset.filter(area_en__icontains=area)
if property_type:
queryset = queryset.filter(property_type=property_type)
if start_date:
queryset = queryset.filter(instance_date__gte=start_date)
if end_date:
queryset = queryset.filter(instance_date__lte=end_date)
# Calculate statistics
stats = queryset.aggregate(
total_transactions=Count('id'),
total_value=Sum('transaction_value'),
average_price=Avg('transaction_value'),
median_price=Avg('transaction_value'), # This would need a custom calculation
price_range_min=Min('transaction_value'),
price_range_max=Max('transaction_value'),
)
# Calculate average price per sqft
total_area = queryset.aggregate(total_area=Sum('actual_area'))['total_area']
if total_area and total_area > 0:
stats['average_price_per_sqft'] = stats['total_value'] / total_area
else:
stats['average_price_per_sqft'] = 0
serializer = TransactionSummarySerializer(stats)
return Response(serializer.data)
except Exception as e:
logger.error(f'Error getting transaction summary: {e}')
return Response(
{'error': 'Failed to get transaction summary'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def area_statistics(request):
"""Get statistics by area."""
try:
# Get query parameters
property_type = request.query_params.get('property_type')
start_date = request.query_params.get('start_date')
end_date = request.query_params.get('end_date')
limit = int(request.query_params.get('limit', 20))
# Build queryset
queryset = Transaction.objects.all()
if property_type:
queryset = queryset.filter(property_type=property_type)
if start_date:
queryset = queryset.filter(instance_date__gte=start_date)
if end_date:
queryset = queryset.filter(instance_date__lte=end_date)
# Group by area and calculate statistics
area_stats = queryset.values('area_en').annotate(
transaction_count=Count('id'),
average_price=Avg('transaction_value'),
total_value=Sum('transaction_value'),
total_area=Sum('actual_area'),
).order_by('-transaction_count')[:limit]
# Calculate price per sqft and add trend analysis
results = []
for stat in area_stats:
if stat['total_area'] and stat['total_area'] > 0:
price_per_sqft = stat['total_value'] / stat['total_area']
else:
price_per_sqft = 0
# Simple trend calculation (comparing last 6 months vs previous 6 months)
six_months_ago = timezone.now() - timedelta(days=180)
twelve_months_ago = timezone.now() - timedelta(days=365)
recent_avg = queryset.filter(
area_en=stat['area_en'],
instance_date__gte=six_months_ago
).aggregate(avg=Avg('transaction_value'))['avg'] or 0
previous_avg = queryset.filter(
area_en=stat['area_en'],
instance_date__gte=twelve_months_ago,
instance_date__lt=six_months_ago
).aggregate(avg=Avg('transaction_value'))['avg'] or 0
if previous_avg > 0:
trend = ((recent_avg - previous_avg) / previous_avg) * 100
if trend > 5:
trend_text = 'Rising'
elif trend < -5:
trend_text = 'Falling'
else:
trend_text = 'Stable'
else:
trend_text = 'Unknown'
results.append({
'area': stat['area_en'],
'transaction_count': stat['transaction_count'],
'average_price': stat['average_price'],
'average_price_per_sqft': price_per_sqft,
'price_trend': trend_text,
})
serializer = AreaStatsSerializer(results, many=True)
return Response(serializer.data)
except Exception as e:
logger.error(f'Error getting area statistics: {e}')
return Response(
{'error': 'Failed to get area statistics'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def property_type_statistics(request):
"""Get statistics by property type."""
try:
# Get query parameters
area = request.query_params.get('area')
start_date = request.query_params.get('start_date')
end_date = request.query_params.get('end_date')
# Build queryset
queryset = Transaction.objects.all()
if area:
queryset = queryset.filter(area_en__icontains=area)
if start_date:
queryset = queryset.filter(instance_date__gte=start_date)
if end_date:
queryset = queryset.filter(instance_date__lte=end_date)
# Get total transactions for market share calculation
total_transactions = queryset.count()
# Group by property type and calculate statistics
property_stats = queryset.values('property_type').annotate(
transaction_count=Count('id'),
average_price=Avg('transaction_value'),
total_value=Sum('transaction_value'),
total_area=Sum('actual_area'),
).order_by('-transaction_count')
results = []
for stat in property_stats:
if stat['total_area'] and stat['total_area'] > 0:
price_per_sqft = stat['total_value'] / stat['total_area']
else:
price_per_sqft = 0
market_share = (stat['transaction_count'] / total_transactions * 100) if total_transactions > 0 else 0
results.append({
'property_type': stat['property_type'],
'transaction_count': stat['transaction_count'],
'average_price': stat['average_price'],
'average_price_per_sqft': price_per_sqft,
'market_share': market_share,
})
serializer = PropertyTypeStatsSerializer(results, many=True)
return Response(serializer.data)
except Exception as e:
logger.error(f'Error getting property type statistics: {e}')
return Response(
{'error': 'Failed to get property type statistics'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def time_series_data(request):
"""Get time series data for charts."""
try:
# Get query parameters
area = request.query_params.get('area')
property_type = request.query_params.get('property_type')
start_date = request.query_params.get('start_date')
end_date = request.query_params.get('end_date')
group_by = request.query_params.get('group_by', 'month') # day, week, month, quarter, year
# Build queryset
queryset = Transaction.objects.all()
if area:
queryset = queryset.filter(area_en__icontains=area)
if property_type:
queryset = queryset.filter(property_type=property_type)
if start_date:
queryset = queryset.filter(instance_date__gte=start_date)
if end_date:
queryset = queryset.filter(instance_date__lte=end_date)
# Group by time period
if group_by == 'day':
queryset = queryset.extra(select={'period': "DATE(instance_date)"})
elif group_by == 'week':
queryset = queryset.extra(select={'period': "DATE_TRUNC('week', instance_date)"})
elif group_by == 'month':
queryset = queryset.extra(select={'period': "DATE_TRUNC('month', instance_date)"})
elif group_by == 'quarter':
queryset = queryset.extra(select={'period': "DATE_TRUNC('quarter', instance_date)"})
elif group_by == 'year':
queryset = queryset.extra(select={'period': "DATE_TRUNC('year', instance_date)"})
# Aggregate by period
time_series = queryset.values('period').annotate(
value=Avg('transaction_value'),
count=Count('id'),
).order_by('period')
results = []
for item in time_series:
results.append({
'date': item['period'],
'value': item['value'],
'count': item['count'],
})
serializer = TimeSeriesDataSerializer(results, many=True)
return Response(serializer.data)
except Exception as e:
logger.error(f'Error getting time series data: {e}')
return Response(
{'error': 'Failed to get time series data'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def generate_forecast(request):
"""Generate property price forecast."""
try:
serializer = ForecastRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# Check if user has forecast permissions
user = request.user
limits = user.get_usage_limits()
if not limits.get('forecast_requests_per_month', 0):
return Response(
{'error': 'Forecast requests not available for your subscription'},
status=status.HTTP_403_FORBIDDEN
)
# Generate forecast using forecasting service
forecasting_service = ForecastingService()
forecast_data = forecasting_service.generate_forecast(
area_en=serializer.validated_data['area_en'],
property_type=serializer.validated_data['property_type'],
property_sub_type=serializer.validated_data.get('property_sub_type', ''),
forecast_periods=serializer.validated_data['forecast_periods'],
confidence_level=float(serializer.validated_data['confidence_level'])
)
return Response(forecast_data)
except Exception as e:
logger.error(f'Error generating forecast: {e}')
return Response(
{'error': 'Failed to generate forecast'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def market_analysis(request):
"""Get comprehensive market analysis."""
try:
# Get query parameters
area = request.query_params.get('area')
property_type = request.query_params.get('property_type')
if not area or not property_type:
return Response(
{'error': 'Area and property_type parameters are required'},
status=status.HTTP_400_BAD_REQUEST
)
# Use analytics service for comprehensive analysis
analytics_service = AnalyticsService()
analysis = analytics_service.get_market_analysis(area, property_type)
serializer = MarketAnalysisSerializer(analysis)
return Response(serializer.data)
except Exception as e:
logger.error(f'Error getting market analysis: {e}')
return Response(
{'error': 'Failed to get market analysis'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_time_series_data(request):
"""Get time series data for charts."""
try:
from datetime import datetime, timedelta
import random
start_date = request.query_params.get('start_date')
end_date = request.query_params.get('end_date')
group_by = request.query_params.get('group_by', 'day')
if not start_date:
start_date = (timezone.now() - timedelta(days=30)).date()
if not end_date:
end_date = timezone.now().date()
# Create sample data for demonstration
data = []
current_date = datetime.strptime(str(start_date), '%Y-%m-%d')
end_date_obj = datetime.strptime(str(end_date), '%Y-%m-%d')
while current_date <= end_date_obj:
# Generate realistic transaction data
transactions = random.randint(0, 15)
value = random.randint(500000, 5000000)
data.append({
'date': current_date.strftime('%Y-%m-%d'),
'transactions': transactions,
'value': value,
'average_value': value // max(transactions, 1)
})
if group_by == 'day':
current_date += timedelta(days=1)
elif group_by == 'week':
current_date += timedelta(weeks=1)
else: # month
current_date += timedelta(days=30)
return Response(data)
except Exception as e:
logger.error(f"Error getting time series data: {str(e)}")
return Response(
{'error': 'Failed to get time series data'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_area_stats(request):
"""Get area statistics for charts."""
try:
start_date = request.query_params.get('start_date')
end_date = request.query_params.get('end_date')
limit = int(request.query_params.get('limit', 10))
if not start_date:
start_date = (timezone.now() - timedelta(days=30)).date()
if not end_date:
end_date = timezone.now().date()
# Get real area data from transactions
queryset = Transaction.objects.filter(
instance_date__date__range=[start_date, end_date]
).exclude(area_en__isnull=True).exclude(area_en='')
area_stats = queryset.values('area_en').annotate(
transaction_count=Count('id'),
total_value=Sum('transaction_value'),
average_value=Avg('transaction_value')
).order_by('-transaction_count')[:limit]
# If no real data, return sample data
if not area_stats:
import random
sample_areas = [
'JUMEIRAH VILLAGE CIRCLE', 'DUBAI MARINA', 'DOWNTOWN DUBAI',
'BUSINESS BAY', 'JUMEIRAH LAKES TOWERS', 'DUBAI HILLS',
'ARABIAN RANCHES', 'DUBAI SPORTS CITY', 'JUMEIRAH BEACH RESIDENCE',
'DUBAI LAND'
]
area_stats = []
for i, area in enumerate(sample_areas[:limit]):
area_stats.append({
'area_en': area,
'transaction_count': random.randint(5, 50),
'total_value': random.randint(1000000, 10000000),
'average_value': random.randint(500000, 2000000)
})
return Response(area_stats)
except Exception as e:
logger.error(f"Error getting area stats: {str(e)}")
return Response(
{'error': 'Failed to get area stats'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([AllowAny])
def broker_statistics(request):
"""Get broker statistics for charts."""
try:
# Get gender distribution
gender_stats = Broker.objects.values('gender').annotate(
count=Count('id')
).order_by('-count')
# Get top real estate companies
company_stats = Broker.objects.values('real_estate_name_en').annotate(
broker_count=Count('id')
).exclude(real_estate_name_en__isnull=True).exclude(real_estate_name_en='').order_by('-broker_count')[:10]
return Response({
'gender_distribution': list(gender_stats),
'top_companies': list(company_stats)
})
except Exception as e:
logger.error(f"Error getting broker statistics: {e}")
return Response(
{'error': 'Failed to get broker statistics'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([AllowAny])
def project_statistics(request):
"""Get project statistics for charts."""
try:
# Get project status distribution
status_stats = Project.objects.values('project_status').annotate(
count=Count('id')
).order_by('-count')
# Get completion rate ranges
completion_ranges = [
{'range': '0-25%', 'count': Project.objects.filter(percent_completed__gte=0, percent_completed__lte=25).count()},
{'range': '26-50%', 'count': Project.objects.filter(percent_completed__gte=26, percent_completed__lte=50).count()},
{'range': '51-75%', 'count': Project.objects.filter(percent_completed__gte=51, percent_completed__lte=75).count()},
{'range': '76-100%', 'count': Project.objects.filter(percent_completed__gte=76, percent_completed__lte=100).count()},
]
return Response({
'status_distribution': list(status_stats),
'completion_rates': completion_ranges
})
except Exception as e:
logger.error(f"Error getting project statistics: {e}")
return Response(
{'error': 'Failed to get project statistics'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([AllowAny])
def land_statistics(request):
"""Get land statistics for charts."""
try:
# Get land type distribution
type_stats = Land.objects.values('land_type').annotate(
count=Count('id')
).order_by('-count')
# Get top areas by land count
area_stats = Land.objects.values('area_en').annotate(
land_count=Count('id')
).exclude(area_en__isnull=True).exclude(area_en='').order_by('-land_count')[:10]
return Response({
'type_distribution': list(type_stats),
'area_distribution': list(area_stats)
})
except Exception as e:
logger.error(f"Error getting land statistics: {e}")
return Response(
{'error': 'Failed to get land statistics'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([AllowAny])
def valuation_statistics(request):
"""Get valuation statistics for charts."""
try:
# Get value trends by month
value_trends = Valuation.objects.extra(
select={'month': "DATE_TRUNC('month', instance_date)"}
).values('month').annotate(
avg_value=Avg('property_total_value')
).order_by('month')[:12] # Last 12 months
# Get top valued areas
area_stats = Valuation.objects.values('area_en').annotate(
avg_value=Avg('property_total_value')
).exclude(area_en__isnull=True).exclude(area_en='').order_by('-avg_value')[:10]
return Response({
'value_trends': list(value_trends),
'top_valued_areas': list(area_stats)
})
except Exception as e:
logger.error(f"Error getting valuation statistics: {e}")
return Response(
{'error': 'Failed to get valuation statistics'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
@permission_classes([AllowAny])
def rent_statistics(request):
"""Get rent statistics for charts."""
try:
# Get rental property types
property_stats = Rent.objects.values('property_type').annotate(
count=Count('id')
).order_by('-count')
# Get average rental prices by area
area_stats = Rent.objects.values('area_en').annotate(
avg_rent=Avg('contract_amount')
).exclude(area_en__isnull=True).exclude(area_en='').order_by('-avg_rent')[:10]
return Response({
'property_types': list(property_stats),
'area_prices': list(area_stats)
})
except Exception as e:
logger.error(f"Error getting rent statistics: {e}")
return Response(
{'error': 'Failed to get rent statistics'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)