Improving parameters
This commit is contained in:
+290
-70
@@ -112,46 +112,79 @@ def get_street_deals(polygon_id: str, limit: int = 100) -> str:
|
|||||||
return f"Error fetching street deals: {str(e)}"
|
return f"Error fetching street deals: {str(e)}"
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def find_recent_deals_for_address(address: str, years_back: int = 2) -> str:
|
def find_recent_deals_for_address(address: str, years_back: int = 2, radius_meters: int = 30, max_deals: int = 200) -> str:
|
||||||
"""Find recent real estate deals for a specific address.
|
"""Find recent real estate deals for a specific address.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
address: The address to search for (in Hebrew or English)
|
address: The address to search for (in Hebrew or English)
|
||||||
years_back: How many years back to search (default: 2)
|
years_back: How many years back to search (default: 2)
|
||||||
|
radius_meters: Search radius in meters from the address (default: 30)
|
||||||
|
Small radius since street deals cover the entire street anyway
|
||||||
|
max_deals: Maximum number of deals to return (default: 200)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
JSON string containing recent real estate deals for the address
|
JSON string containing recent real estate deals for the address
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
deals = client.find_recent_deals_for_address(address, years_back)
|
deals = client.find_recent_deals_for_address(address, years_back, radius_meters, max_deals)
|
||||||
|
|
||||||
if not deals:
|
if not deals:
|
||||||
return f"No deals found for address '{address}'"
|
return f"No deals found for address '{address}'"
|
||||||
|
|
||||||
# Calculate basic statistics
|
# Calculate comprehensive statistics
|
||||||
prices = [deal.get("dealAmount", 0) for deal in deals if deal.get("dealAmount")]
|
prices = [deal.get("dealAmount", 0) for deal in deals if deal.get("dealAmount")]
|
||||||
areas = [deal.get("assetArea", 0) for deal in deals if deal.get("assetArea")]
|
areas = [deal.get("assetArea", 0) for deal in deals if deal.get("assetArea")]
|
||||||
|
price_per_sqm_values = [deal.get("price_per_sqm", 0) for deal in deals if deal.get("price_per_sqm")]
|
||||||
|
|
||||||
|
# Separate building, street and neighborhood deals for analysis
|
||||||
|
building_deals = [deal for deal in deals if deal.get("deal_source") == "same_building"]
|
||||||
|
street_deals = [deal for deal in deals if deal.get("deal_source") == "street"]
|
||||||
|
neighborhood_deals = [deal for deal in deals if deal.get("deal_source") == "neighborhood"]
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"deal_breakdown": {
|
||||||
|
"total_deals": len(deals),
|
||||||
|
"same_building_deals": len(building_deals),
|
||||||
|
"street_deals": len(street_deals),
|
||||||
|
"neighborhood_deals": len(neighborhood_deals),
|
||||||
|
"same_building_percentage": round((len(building_deals) / len(deals)) * 100, 1) if deals else 0,
|
||||||
|
"street_emphasis_percentage": round((len(street_deals) / len(deals)) * 100, 1) if deals else 0,
|
||||||
|
"neighborhood_percentage": round((len(neighborhood_deals) / len(deals)) * 100, 1) if deals else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stats = {}
|
|
||||||
if prices:
|
if prices:
|
||||||
stats["price_stats"] = {
|
stats["price_stats"] = {
|
||||||
"average_price": sum(prices) / len(prices),
|
"average_price": round(sum(prices) / len(prices), 0),
|
||||||
"min_price": min(prices),
|
"min_price": min(prices),
|
||||||
"max_price": max(prices),
|
"max_price": max(prices),
|
||||||
"total_deals": len(prices)
|
"median_price": sorted(prices)[len(prices)//2] if prices else 0,
|
||||||
|
"total_volume": sum(prices)
|
||||||
}
|
}
|
||||||
|
|
||||||
if areas:
|
if areas:
|
||||||
stats["area_stats"] = {
|
stats["area_stats"] = {
|
||||||
"average_area": sum(areas) / len(areas),
|
"average_area": round(sum(areas) / len(areas), 1),
|
||||||
"min_area": min(areas),
|
"min_area": min(areas),
|
||||||
"max_area": max(areas)
|
"max_area": max(areas),
|
||||||
|
"median_area": sorted(areas)[len(areas)//2] if areas else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if price_per_sqm_values:
|
||||||
|
stats["price_per_sqm_stats"] = {
|
||||||
|
"average_price_per_sqm": round(sum(price_per_sqm_values) / len(price_per_sqm_values), 0),
|
||||||
|
"min_price_per_sqm": round(min(price_per_sqm_values), 0),
|
||||||
|
"max_price_per_sqm": round(max(price_per_sqm_values), 0),
|
||||||
|
"median_price_per_sqm": round(sorted(price_per_sqm_values)[len(price_per_sqm_values)//2], 0) if price_per_sqm_values else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
"search_address": address,
|
"search_parameters": {
|
||||||
"years_back": years_back,
|
"address": address,
|
||||||
"total_deals": len(deals),
|
"years_back": years_back,
|
||||||
|
"radius_meters": radius_meters,
|
||||||
|
"max_deals": max_deals
|
||||||
|
},
|
||||||
"market_statistics": stats,
|
"market_statistics": stats,
|
||||||
"deals": deals
|
"deals": deals
|
||||||
}, ensure_ascii=False, indent=2)
|
}, ensure_ascii=False, indent=2)
|
||||||
@@ -177,9 +210,31 @@ def get_neighborhood_deals(polygon_id: str, limit: int = 100) -> str:
|
|||||||
if not deals:
|
if not deals:
|
||||||
return f"No deals found for polygon ID {polygon_id}"
|
return f"No deals found for polygon ID {polygon_id}"
|
||||||
|
|
||||||
|
# Add price per sqm calculation for each deal
|
||||||
|
for deal in deals:
|
||||||
|
price = deal.get('dealAmount', 0)
|
||||||
|
area = deal.get('assetArea', 0)
|
||||||
|
if isinstance(price, (int, float)) and isinstance(area, (int, float)) and area > 0:
|
||||||
|
deal['price_per_sqm'] = round(price / area, 2)
|
||||||
|
else:
|
||||||
|
deal['price_per_sqm'] = None
|
||||||
|
|
||||||
|
# Calculate basic statistics including price per sqm
|
||||||
|
prices = [deal.get("dealAmount", 0) for deal in deals if deal.get("dealAmount")]
|
||||||
|
price_per_sqm_values = [deal.get("price_per_sqm", 0) for deal in deals if deal.get("price_per_sqm")]
|
||||||
|
|
||||||
|
stats = {}
|
||||||
|
if price_per_sqm_values:
|
||||||
|
stats["price_per_sqm_stats"] = {
|
||||||
|
"average_price_per_sqm": round(sum(price_per_sqm_values) / len(price_per_sqm_values), 0),
|
||||||
|
"min_price_per_sqm": round(min(price_per_sqm_values), 0),
|
||||||
|
"max_price_per_sqm": round(max(price_per_sqm_values), 0)
|
||||||
|
}
|
||||||
|
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
"total_deals": len(deals),
|
"total_deals": len(deals),
|
||||||
"polygon_id": polygon_id,
|
"polygon_id": polygon_id,
|
||||||
|
"market_statistics": stats,
|
||||||
"deals": deals
|
"deals": deals
|
||||||
}, ensure_ascii=False, indent=2)
|
}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
@@ -188,94 +243,225 @@ def get_neighborhood_deals(polygon_id: str, limit: int = 100) -> str:
|
|||||||
return f"Error fetching neighborhood deals: {str(e)}"
|
return f"Error fetching neighborhood deals: {str(e)}"
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def analyze_market_trends(address: str, years_back: int = 3) -> str:
|
def analyze_market_trends(address: str, years_back: int = 3, radius_meters: int = 300, max_deals: int = 500) -> str:
|
||||||
"""Analyze market trends and price patterns for an area.
|
"""Analyze market trends and price patterns for an area with comprehensive data.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
address: The address to analyze trends around
|
address: The address to analyze trends around
|
||||||
years_back: How many years of data to analyze (default: 3)
|
years_back: How many years of data to analyze (default: 3)
|
||||||
|
radius_meters: Search radius in meters from the address (default: 300, larger for trend analysis)
|
||||||
|
max_deals: Maximum number of deals to analyze (default: 500)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
JSON string containing market trend analysis including:
|
JSON string containing comprehensive market trend analysis including:
|
||||||
- Price trends over time
|
- Detailed price trends over time
|
||||||
- Average prices by property type
|
- Average prices by property type
|
||||||
- Market activity levels
|
- Market activity levels and patterns
|
||||||
- Price per square meter trends
|
- Price per square meter trends
|
||||||
|
- Seasonal patterns
|
||||||
|
- Market velocity indicators
|
||||||
|
- Comparative neighborhood analysis
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Get deals for the address
|
# Get deals for the address with larger radius for trend analysis
|
||||||
deals = client.find_recent_deals_for_address(address, years_back)
|
deals = client.find_recent_deals_for_address(address, years_back, radius_meters, max_deals)
|
||||||
|
|
||||||
if not deals:
|
if not deals:
|
||||||
return f"No deals found for market analysis near '{address}'"
|
return f"No deals found for comprehensive market analysis near '{address}'"
|
||||||
|
|
||||||
# Analyze trends by year
|
# Comprehensive analysis structure
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
yearly_data = defaultdict(list)
|
yearly_data = defaultdict(list)
|
||||||
property_types: Dict[str, int] = defaultdict(int)
|
monthly_data = defaultdict(list)
|
||||||
neighborhoods = set()
|
property_types: Dict[str, List[Dict]] = defaultdict(list)
|
||||||
|
neighborhoods = defaultdict(list)
|
||||||
|
quarterly_data = defaultdict(list)
|
||||||
|
|
||||||
|
# Process each deal for comprehensive analysis
|
||||||
for deal in deals:
|
for deal in deals:
|
||||||
date_str = deal.get('dealDate', '')
|
date_str = deal.get('dealDate', '')
|
||||||
if date_str:
|
if not date_str:
|
||||||
year = date_str[:4]
|
continue
|
||||||
price = deal.get('dealAmount')
|
|
||||||
area = deal.get('assetArea')
|
|
||||||
prop_type = deal.get('assetTypeHeb', deal.get('propertyTypeDescription', 'Unknown'))
|
|
||||||
neighborhood = deal.get('settlementNameHeb', deal.get('neighborhood'))
|
|
||||||
|
|
||||||
if neighborhood:
|
year = date_str[:4]
|
||||||
neighborhoods.add(neighborhood)
|
month = date_str[:7] # YYYY-MM
|
||||||
|
quarter = f"{year}-Q{((int(date_str[5:7]) - 1) // 3) + 1}" if len(date_str) >= 7 else None
|
||||||
if isinstance(price, (int, float)) and isinstance(area, (int, float)) and area > 0:
|
|
||||||
yearly_data[year].append({
|
price = deal.get('dealAmount')
|
||||||
'price': price,
|
area = deal.get('assetArea')
|
||||||
'area': area,
|
price_per_sqm = deal.get('price_per_sqm')
|
||||||
'price_per_sqm': price / area
|
prop_type = deal.get('assetTypeHeb', deal.get('propertyTypeDescription', 'לא ידוע'))
|
||||||
})
|
neighborhood = deal.get('settlementNameHeb', deal.get('neighborhood', 'לא ידוע'))
|
||||||
property_types[prop_type] += 1
|
deal_source = deal.get('deal_source', 'unknown')
|
||||||
|
|
||||||
|
deal_data = {
|
||||||
|
'price': price,
|
||||||
|
'area': area,
|
||||||
|
'price_per_sqm': price_per_sqm,
|
||||||
|
'property_type': prop_type,
|
||||||
|
'neighborhood': neighborhood,
|
||||||
|
'deal_source': deal_source,
|
||||||
|
'date': date_str
|
||||||
|
}
|
||||||
|
|
||||||
|
if isinstance(price, (int, float)) and isinstance(area, (int, float)) and area > 0:
|
||||||
|
yearly_data[year].append(deal_data)
|
||||||
|
monthly_data[month].append(deal_data)
|
||||||
|
if quarter:
|
||||||
|
quarterly_data[quarter].append(deal_data)
|
||||||
|
property_types[prop_type].append(deal_data)
|
||||||
|
neighborhoods[neighborhood].append(deal_data)
|
||||||
|
|
||||||
# Calculate yearly trends
|
# Calculate comprehensive yearly trends
|
||||||
yearly_trends = {}
|
yearly_trends = {}
|
||||||
for year, year_deals in yearly_data.items():
|
for year, year_deals in yearly_data.items():
|
||||||
if year_deals:
|
if year_deals:
|
||||||
|
prices = [d['price'] for d in year_deals if d['price']]
|
||||||
|
price_per_sqm_vals = [d['price_per_sqm'] for d in year_deals if d['price_per_sqm']]
|
||||||
|
areas = [d['area'] for d in year_deals if d['area']]
|
||||||
|
building_deals = [d for d in year_deals if d['deal_source'] == 'same_building']
|
||||||
|
street_deals = [d for d in year_deals if d['deal_source'] == 'street']
|
||||||
|
neighborhood_deals = [d for d in year_deals if d['deal_source'] == 'neighborhood']
|
||||||
|
|
||||||
yearly_trends[year] = {
|
yearly_trends[year] = {
|
||||||
"average_price": sum(d['price'] for d in year_deals) / len(year_deals),
|
"deal_count": len(year_deals),
|
||||||
"min_price": min(d['price'] for d in year_deals),
|
"same_building_deals_count": len(building_deals),
|
||||||
"max_price": max(d['price'] for d in year_deals),
|
"street_deals_count": len(street_deals),
|
||||||
"average_area": sum(d['area'] for d in year_deals) / len(year_deals),
|
"neighborhood_deals_count": len(neighborhood_deals),
|
||||||
"average_price_per_sqm": sum(d['price_per_sqm'] for d in year_deals) / len(year_deals),
|
"same_building_percentage": round((len(building_deals) / len(year_deals)) * 100, 1),
|
||||||
"deal_count": len(year_deals)
|
"street_deals_percentage": round((len(street_deals) / len(year_deals)) * 100, 1),
|
||||||
|
"average_price": round(sum(prices) / len(prices), 0) if prices else 0,
|
||||||
|
"median_price": round(sorted(prices)[len(prices)//2], 0) if prices else 0,
|
||||||
|
"min_price": min(prices) if prices else 0,
|
||||||
|
"max_price": max(prices) if prices else 0,
|
||||||
|
"price_std_dev": round((sum([(p - sum(prices)/len(prices))**2 for p in prices]) / len(prices))**0.5, 0) if len(prices) > 1 else 0,
|
||||||
|
"average_area": round(sum(areas) / len(areas), 1) if areas else 0,
|
||||||
|
"average_price_per_sqm": round(sum(price_per_sqm_vals) / len(price_per_sqm_vals), 0) if price_per_sqm_vals else 0,
|
||||||
|
"median_price_per_sqm": round(sorted(price_per_sqm_vals)[len(price_per_sqm_vals)//2], 0) if price_per_sqm_vals else 0,
|
||||||
|
"total_market_volume": sum(prices) if prices else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Calculate price trend direction
|
# Calculate quarterly trends for seasonality analysis
|
||||||
|
quarterly_trends = {}
|
||||||
|
for quarter, quarter_deals in quarterly_data.items():
|
||||||
|
if quarter_deals:
|
||||||
|
prices = [d['price'] for d in quarter_deals if d['price']]
|
||||||
|
price_per_sqm_vals = [d['price_per_sqm'] for d in quarter_deals if d['price_per_sqm']]
|
||||||
|
|
||||||
|
quarterly_trends[quarter] = {
|
||||||
|
"deal_count": len(quarter_deals),
|
||||||
|
"average_price": round(sum(prices) / len(prices), 0) if prices else 0,
|
||||||
|
"average_price_per_sqm": round(sum(price_per_sqm_vals) / len(price_per_sqm_vals), 0) if price_per_sqm_vals else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Property type analysis
|
||||||
|
property_type_analysis = {}
|
||||||
|
for prop_type, type_deals in property_types.items():
|
||||||
|
if type_deals:
|
||||||
|
prices = [d['price'] for d in type_deals if d['price']]
|
||||||
|
price_per_sqm_vals = [d['price_per_sqm'] for d in type_deals if d['price_per_sqm']]
|
||||||
|
areas = [d['area'] for d in type_deals if d['area']]
|
||||||
|
|
||||||
|
property_type_analysis[prop_type] = {
|
||||||
|
"deal_count": len(type_deals),
|
||||||
|
"market_share_percentage": round((len(type_deals) / len(deals)) * 100, 1),
|
||||||
|
"average_price": round(sum(prices) / len(prices), 0) if prices else 0,
|
||||||
|
"average_area": round(sum(areas) / len(areas), 1) if areas else 0,
|
||||||
|
"average_price_per_sqm": round(sum(price_per_sqm_vals) / len(price_per_sqm_vals), 0) if price_per_sqm_vals else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Neighborhood comparison analysis
|
||||||
|
neighborhood_analysis = {}
|
||||||
|
for neighborhood, neighborhood_deals in neighborhoods.items():
|
||||||
|
if neighborhood_deals and len(neighborhood_deals) >= 3: # Only include neighborhoods with sufficient data
|
||||||
|
prices = [d['price'] for d in neighborhood_deals if d['price']]
|
||||||
|
price_per_sqm_vals = [d['price_per_sqm'] for d in neighborhood_deals if d['price_per_sqm']]
|
||||||
|
|
||||||
|
neighborhood_analysis[neighborhood] = {
|
||||||
|
"deal_count": len(neighborhood_deals),
|
||||||
|
"average_price": round(sum(prices) / len(prices), 0) if prices else 0,
|
||||||
|
"average_price_per_sqm": round(sum(price_per_sqm_vals) / len(price_per_sqm_vals), 0) if price_per_sqm_vals else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Market trend direction analysis
|
||||||
price_trend_analysis = {}
|
price_trend_analysis = {}
|
||||||
years_sorted = sorted(yearly_trends.keys())
|
years_sorted = sorted(yearly_trends.keys())
|
||||||
if len(years_sorted) >= 2:
|
if len(years_sorted) >= 2:
|
||||||
first_year_avg = yearly_trends[years_sorted[0]]['average_price_per_sqm']
|
first_year_data = yearly_trends[years_sorted[0]]
|
||||||
last_year_avg = yearly_trends[years_sorted[-1]]['average_price_per_sqm']
|
last_year_data = yearly_trends[years_sorted[-1]]
|
||||||
|
|
||||||
trend_percentage = ((last_year_avg - first_year_avg) / first_year_avg) * 100
|
# Price trend
|
||||||
trend_direction = "rising" if trend_percentage > 5 else "declining" if trend_percentage < -5 else "stable"
|
first_year_avg = first_year_data['average_price_per_sqm']
|
||||||
|
last_year_avg = last_year_data['average_price_per_sqm']
|
||||||
|
|
||||||
price_trend_analysis = {
|
if first_year_avg > 0:
|
||||||
"trend_direction": trend_direction,
|
price_trend_percentage = ((last_year_avg - first_year_avg) / first_year_avg) * 100
|
||||||
"trend_percentage": round(trend_percentage, 1),
|
price_trend_direction = "עולה" if price_trend_percentage > 5 else "יורד" if price_trend_percentage < -5 else "יציב"
|
||||||
"first_year": years_sorted[0],
|
|
||||||
"last_year": years_sorted[-1],
|
# Volume trend
|
||||||
"first_year_avg_price_per_sqm": round(first_year_avg, 0),
|
first_year_volume = first_year_data['deal_count']
|
||||||
"last_year_avg_price_per_sqm": round(last_year_avg, 0)
|
last_year_volume = last_year_data['deal_count']
|
||||||
|
volume_trend_percentage = ((last_year_volume - first_year_volume) / first_year_volume) * 100 if first_year_volume > 0 else 0
|
||||||
|
volume_trend_direction = "עולה" if volume_trend_percentage > 10 else "יורד" if volume_trend_percentage < -10 else "יציב"
|
||||||
|
|
||||||
|
price_trend_analysis = {
|
||||||
|
"price_trend_direction": price_trend_direction,
|
||||||
|
"price_trend_percentage": round(price_trend_percentage, 1),
|
||||||
|
"volume_trend_direction": volume_trend_direction,
|
||||||
|
"volume_trend_percentage": round(volume_trend_percentage, 1),
|
||||||
|
"analysis_period": f"{years_sorted[0]} - {years_sorted[-1]}",
|
||||||
|
"first_year_avg_price_per_sqm": round(first_year_avg, 0),
|
||||||
|
"last_year_avg_price_per_sqm": round(last_year_avg, 0),
|
||||||
|
"total_price_change": round(last_year_avg - first_year_avg, 0),
|
||||||
|
"annualized_price_growth": round(price_trend_percentage / len(years_sorted), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Market velocity indicators
|
||||||
|
market_velocity = {
|
||||||
|
"average_deals_per_month": round(len(deals) / (years_back * 12), 1),
|
||||||
|
"peak_activity_quarter": max(quarterly_trends.keys(), key=lambda q: quarterly_trends[q]['deal_count']) if quarterly_trends else None,
|
||||||
|
"lowest_activity_quarter": min(quarterly_trends.keys(), key=lambda q: quarterly_trends[q]['deal_count']) if quarterly_trends else None
|
||||||
|
}
|
||||||
|
|
||||||
|
# Price distribution analysis
|
||||||
|
all_prices_per_sqm = [deal.get('price_per_sqm', 0) for deal in deals if deal.get('price_per_sqm')]
|
||||||
|
price_distribution = {}
|
||||||
|
if all_prices_per_sqm:
|
||||||
|
sorted_prices = sorted(all_prices_per_sqm)
|
||||||
|
price_distribution = {
|
||||||
|
"25th_percentile": round(sorted_prices[len(sorted_prices)//4], 0),
|
||||||
|
"75th_percentile": round(sorted_prices[3*len(sorted_prices)//4], 0),
|
||||||
|
"price_range_iqr": round(sorted_prices[3*len(sorted_prices)//4] - sorted_prices[len(sorted_prices)//4], 0),
|
||||||
|
"coefficient_of_variation": round((yearly_trends[years_sorted[-1]]['price_std_dev'] / yearly_trends[years_sorted[-1]]['average_price']) * 100, 1) if years_sorted and yearly_trends[years_sorted[-1]]['average_price'] > 0 else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
"analysis_address": address,
|
"analysis_parameters": {
|
||||||
"analysis_period_years": years_back,
|
"address": address,
|
||||||
"total_deals_analyzed": len(deals),
|
"analysis_period_years": years_back,
|
||||||
"neighborhoods": list(neighborhoods),
|
"search_radius_meters": radius_meters,
|
||||||
"property_types": dict(property_types),
|
"max_deals_analyzed": max_deals
|
||||||
|
},
|
||||||
|
"market_overview": {
|
||||||
|
"total_deals_analyzed": len(deals),
|
||||||
|
"unique_neighborhoods": len(neighborhoods),
|
||||||
|
"unique_property_types": len(property_types),
|
||||||
|
"data_coverage_years": len(yearly_trends)
|
||||||
|
},
|
||||||
"yearly_trends": yearly_trends,
|
"yearly_trends": yearly_trends,
|
||||||
"price_trend_analysis": price_trend_analysis
|
"quarterly_trends": quarterly_trends,
|
||||||
|
"property_type_analysis": property_type_analysis,
|
||||||
|
"neighborhood_comparison": neighborhood_analysis,
|
||||||
|
"market_trend_analysis": price_trend_analysis,
|
||||||
|
"market_velocity_indicators": market_velocity,
|
||||||
|
"price_distribution_analysis": price_distribution,
|
||||||
|
"detailed_insights": {
|
||||||
|
"most_active_property_type": max(property_type_analysis.keys(), key=lambda pt: property_type_analysis[pt]['deal_count']) if property_type_analysis else None,
|
||||||
|
"highest_value_property_type": max(property_type_analysis.keys(), key=lambda pt: property_type_analysis[pt]['average_price_per_sqm']) if property_type_analysis else None,
|
||||||
|
"most_expensive_neighborhood": max(neighborhood_analysis.keys(), key=lambda n: neighborhood_analysis[n]['average_price_per_sqm']) if neighborhood_analysis else None,
|
||||||
|
"deal_source_breakdown": f"Same building: {len([d for d in deals if d.get('deal_source') == 'same_building'])}, Street: {len([d for d in deals if d.get('deal_source') == 'street'])}, Neighborhood: {len([d for d in deals if d.get('deal_source') == 'neighborhood'])}"
|
||||||
|
}
|
||||||
}, ensure_ascii=False, indent=2)
|
}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -302,27 +488,47 @@ def compare_addresses(addresses: List[str]) -> str:
|
|||||||
if deals:
|
if deals:
|
||||||
prices = [deal.get("dealAmount", 0) for deal in deals if deal.get("dealAmount")]
|
prices = [deal.get("dealAmount", 0) for deal in deals if deal.get("dealAmount")]
|
||||||
areas = [deal.get("assetArea", 0) for deal in deals if deal.get("assetArea")]
|
areas = [deal.get("assetArea", 0) for deal in deals if deal.get("assetArea")]
|
||||||
|
price_per_sqm_values = [deal.get("price_per_sqm", 0) for deal in deals if deal.get("price_per_sqm")]
|
||||||
|
building_deals = [deal for deal in deals if deal.get("deal_source") == "same_building"]
|
||||||
|
street_deals = [deal for deal in deals if deal.get("deal_source") == "street"]
|
||||||
|
neighborhood_deals = [deal for deal in deals if deal.get("deal_source") == "neighborhood"]
|
||||||
|
|
||||||
comparison = {
|
comparison = {
|
||||||
"address": address,
|
"address": address,
|
||||||
"total_deals": len(deals),
|
"total_deals": len(deals),
|
||||||
|
"same_building_deals": len(building_deals),
|
||||||
|
"street_deals": len(street_deals),
|
||||||
|
"neighborhood_deals": len(neighborhood_deals),
|
||||||
|
"same_building_percentage": round((len(building_deals) / len(deals)) * 100, 1) if deals else 0,
|
||||||
|
"street_emphasis_percentage": round((len(street_deals) / len(deals)) * 100, 1) if deals else 0,
|
||||||
"price_stats": {
|
"price_stats": {
|
||||||
"average_price": sum(prices) / len(prices) if prices else 0,
|
"average_price": round(sum(prices) / len(prices), 0) if prices else 0,
|
||||||
"min_price": min(prices) if prices else 0,
|
"min_price": min(prices) if prices else 0,
|
||||||
"max_price": max(prices) if prices else 0
|
"max_price": max(prices) if prices else 0
|
||||||
},
|
},
|
||||||
"area_stats": {
|
"area_stats": {
|
||||||
"average_area": sum(areas) / len(areas) if areas else 0,
|
"average_area": round(sum(areas) / len(areas), 1) if areas else 0,
|
||||||
"min_area": min(areas) if areas else 0,
|
"min_area": min(areas) if areas else 0,
|
||||||
"max_area": max(areas) if areas else 0
|
"max_area": max(areas) if areas else 0
|
||||||
|
},
|
||||||
|
"price_per_sqm_stats": {
|
||||||
|
"average_price_per_sqm": round(sum(price_per_sqm_values) / len(price_per_sqm_values), 0) if price_per_sqm_values else 0,
|
||||||
|
"min_price_per_sqm": round(min(price_per_sqm_values), 0) if price_per_sqm_values else 0,
|
||||||
|
"max_price_per_sqm": round(max(price_per_sqm_values), 0) if price_per_sqm_values else 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
comparison = {
|
comparison = {
|
||||||
"address": address,
|
"address": address,
|
||||||
"total_deals": 0,
|
"total_deals": 0,
|
||||||
|
"same_building_deals": 0,
|
||||||
|
"street_deals": 0,
|
||||||
|
"neighborhood_deals": 0,
|
||||||
|
"same_building_percentage": 0,
|
||||||
|
"street_emphasis_percentage": 0,
|
||||||
"price_stats": {},
|
"price_stats": {},
|
||||||
"area_stats": {}
|
"area_stats": {},
|
||||||
|
"price_per_sqm_stats": {}
|
||||||
}
|
}
|
||||||
|
|
||||||
comparisons.append(comparison)
|
comparisons.append(comparison)
|
||||||
@@ -334,13 +540,27 @@ def compare_addresses(addresses: List[str]) -> str:
|
|||||||
"error": str(e)
|
"error": str(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Rank addresses by average price
|
# Rank addresses by average price per sqm
|
||||||
valid_comparisons = [c for c in comparisons if c.get("price_stats", {}).get("average_price", 0) > 0]
|
valid_comparisons = []
|
||||||
valid_comparisons.sort(key=lambda x: x["price_stats"]["average_price"], reverse=True)
|
for comparison in comparisons:
|
||||||
|
if (isinstance(comparison, dict) and
|
||||||
|
"price_per_sqm_stats" in comparison and
|
||||||
|
isinstance(comparison["price_per_sqm_stats"], dict) and
|
||||||
|
comparison["price_per_sqm_stats"].get("average_price_per_sqm", 0) > 0):
|
||||||
|
valid_comparisons.append(comparison)
|
||||||
|
|
||||||
|
# Sort by price per sqm
|
||||||
|
def get_price_per_sqm(comp: dict) -> float:
|
||||||
|
price_stats = comp.get("price_per_sqm_stats", {})
|
||||||
|
if isinstance(price_stats, dict):
|
||||||
|
return price_stats.get("average_price_per_sqm", 0)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
valid_comparisons.sort(key=get_price_per_sqm, reverse=True)
|
||||||
|
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
"addresses_compared": len(addresses),
|
"addresses_compared": len(addresses),
|
||||||
"ranking_by_average_price": valid_comparisons,
|
"ranking_by_average_price_per_sqm": valid_comparisons,
|
||||||
"all_results": comparisons
|
"all_results": comparisons
|
||||||
}, ensure_ascii=False, indent=2)
|
}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|||||||
+120
-23
@@ -225,18 +225,24 @@ class GovmapClient:
|
|||||||
logger.error(f"Error parsing JSON response: {e}")
|
logger.error(f"Error parsing JSON response: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def find_recent_deals_for_address(self, address: str, years_back: int = 2) -> List[Dict[str, Any]]:
|
def find_recent_deals_for_address(self, address: str, years_back: int = 2,
|
||||||
|
radius: int = 30, max_deals: int = 200) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Find all relevant real estate deals for a given address from the last few years.
|
Find all relevant real estate deals for a given address from the last few years.
|
||||||
|
|
||||||
This is the main use case function that ties everything together.
|
This is the main use case function that ties everything together.
|
||||||
|
Street deals include deals from the same building which get highest priority.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
address: The address to search for
|
address: The address to search for
|
||||||
years_back: How many years back to search (default: 2)
|
years_back: How many years back to search (default: 2)
|
||||||
|
radius: Search radius in meters for initial coordinate search (default: 30)
|
||||||
|
Small radius since street deals cover the entire street anyway
|
||||||
|
max_deals: Maximum number of deals to return (default: 200)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of deals found for the address area
|
List of deals found for the address area, with same building deals prioritized first,
|
||||||
|
then street deals, then neighborhood deals
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If address cannot be found or processed
|
ValueError: If address cannot be found or processed
|
||||||
@@ -268,10 +274,11 @@ class GovmapClient:
|
|||||||
raise ValueError("Invalid coordinate format in autocomplete result")
|
raise ValueError("Invalid coordinate format in autocomplete result")
|
||||||
|
|
||||||
point = (float(coords[0]), float(coords[1]))
|
point = (float(coords[0]), float(coords[1]))
|
||||||
|
search_address_normalized = address.lower().strip()
|
||||||
logger.info(f"Found coordinates: {point}")
|
logger.info(f"Found coordinates: {point}")
|
||||||
|
|
||||||
# Step 2: Get deals by radius to find polygon IDs
|
# Step 2: Get deals by radius to find polygon IDs
|
||||||
nearby_deals = self.get_deals_by_radius(point, radius=30) # Slightly larger radius
|
nearby_deals = self.get_deals_by_radius(point, radius=radius)
|
||||||
|
|
||||||
# Extract unique polygon IDs
|
# Extract unique polygon IDs
|
||||||
polygon_ids = set()
|
polygon_ids = set()
|
||||||
@@ -288,46 +295,136 @@ class GovmapClient:
|
|||||||
end_date_str = end_date.strftime('%Y-%m')
|
end_date_str = end_date.strftime('%Y-%m')
|
||||||
|
|
||||||
# Step 4: Get street and neighborhood deals for each polygon
|
# Step 4: Get street and neighborhood deals for each polygon
|
||||||
all_deals = []
|
# Prioritize: same building (0) > street deals (1) > neighborhood deals (2)
|
||||||
|
building_deals = []
|
||||||
|
street_deals = []
|
||||||
|
neighborhood_deals = []
|
||||||
seen_deals = set() # For deduplication
|
seen_deals = set() # For deduplication
|
||||||
|
|
||||||
for polygon_id in polygon_ids:
|
for polygon_id in polygon_ids:
|
||||||
try:
|
try:
|
||||||
# Get street deals
|
# Get street deals first (higher priority)
|
||||||
street_deals = self.get_street_deals(
|
current_street_deals = self.get_street_deals(
|
||||||
polygon_id, limit=50,
|
polygon_id, limit=max_deals // 2, # Allocate more to street deals
|
||||||
start_date=start_date_str, end_date=end_date_str
|
start_date=start_date_str, end_date=end_date_str
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get neighborhood deals
|
# Get neighborhood deals (lower priority)
|
||||||
neighborhood_deals = self.get_neighborhood_deals(
|
current_neighborhood_deals = self.get_neighborhood_deals(
|
||||||
polygon_id, limit=50,
|
polygon_id, limit=max_deals // 4, # Allocate less to neighborhood deals
|
||||||
start_date=start_date_str, end_date=end_date_str
|
start_date=start_date_str, end_date=end_date_str
|
||||||
)
|
)
|
||||||
|
|
||||||
# Combine deals
|
# Process street deals and separate building deals
|
||||||
combined_deals = street_deals + neighborhood_deals
|
for deal in current_street_deals:
|
||||||
|
|
||||||
# Add to results with deduplication
|
|
||||||
for deal in combined_deals:
|
|
||||||
# Create a unique identifier for the deal
|
|
||||||
deal_id = f"{deal.get('dealId', '')}{deal.get('address', '')}{deal.get('dealDate', '')}"
|
deal_id = f"{deal.get('dealId', '')}{deal.get('address', '')}{deal.get('dealDate', '')}"
|
||||||
|
|
||||||
if deal_id not in seen_deals:
|
if deal_id not in seen_deals:
|
||||||
seen_deals.add(deal_id)
|
seen_deals.add(deal_id)
|
||||||
deal['source_polygon_id'] = polygon_id # Add source for reference
|
deal['source_polygon_id'] = polygon_id
|
||||||
all_deals.append(deal)
|
deal['deal_source'] = 'street'
|
||||||
|
|
||||||
|
# Check if this is from the same building
|
||||||
|
deal_address = deal.get('address', '').lower().strip()
|
||||||
|
if self._is_same_building(search_address_normalized, deal_address):
|
||||||
|
deal['deal_source'] = 'same_building'
|
||||||
|
deal['priority'] = 0 # Highest priority
|
||||||
|
building_deals.append(deal)
|
||||||
|
else:
|
||||||
|
deal['priority'] = 1 # Street deals priority
|
||||||
|
street_deals.append(deal)
|
||||||
|
|
||||||
|
# Add neighborhood deals with lowest priority
|
||||||
|
for deal in current_neighborhood_deals:
|
||||||
|
deal_id = f"{deal.get('dealId', '')}{deal.get('address', '')}{deal.get('dealDate', '')}"
|
||||||
|
if deal_id not in seen_deals:
|
||||||
|
seen_deals.add(deal_id)
|
||||||
|
deal['source_polygon_id'] = polygon_id
|
||||||
|
deal['deal_source'] = 'neighborhood'
|
||||||
|
deal['priority'] = 2 # Lowest priority
|
||||||
|
neighborhood_deals.append(deal)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error processing polygon {polygon_id}: {e}")
|
logger.warning(f"Error processing polygon {polygon_id}: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Step 5: Sort by date (newest first)
|
# Step 5: Combine and prioritize: building deals first, then street, then neighborhood
|
||||||
all_deals.sort(key=lambda x: x.get('dealDate', ''), reverse=True)
|
all_deals = building_deals + street_deals + neighborhood_deals
|
||||||
|
|
||||||
logger.info(f"Found {len(all_deals)} total deals for address: {address}")
|
# Use stable sort: first by date (newest first), then by priority
|
||||||
|
# Since Python's sort is stable, the second sort maintains date order within each priority
|
||||||
|
all_deals.sort(key=lambda x: x.get('dealDate', '1900-01-01'), reverse=True) # Newest first
|
||||||
|
all_deals.sort(key=lambda x: x.get('priority', 3)) # Priority first (0=building, 1=street, 2=neighborhood)
|
||||||
|
|
||||||
|
# Limit to max_deals
|
||||||
|
if len(all_deals) > max_deals:
|
||||||
|
all_deals = all_deals[:max_deals]
|
||||||
|
|
||||||
|
# Add price per square meter calculation
|
||||||
|
for deal in all_deals:
|
||||||
|
price = deal.get('dealAmount', 0)
|
||||||
|
area = deal.get('assetArea', 0)
|
||||||
|
if isinstance(price, (int, float)) and isinstance(area, (int, float)) and area > 0:
|
||||||
|
deal['price_per_sqm'] = round(price / area, 2)
|
||||||
|
else:
|
||||||
|
deal['price_per_sqm'] = None
|
||||||
|
|
||||||
|
logger.info(f"Found {len(all_deals)} total deals for address: {address} "
|
||||||
|
f"(Building: {len(building_deals)}, Street: {len(street_deals)}, Neighborhood: {len(neighborhood_deals)})")
|
||||||
return all_deals
|
return all_deals
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in find_recent_deals_for_address: {e}")
|
logger.error(f"Error in find_recent_deals_for_address: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def _is_same_building(self, search_address: str, deal_address: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a deal is from the same building as the search address.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
search_address: The normalized search address (lowercase, stripped)
|
||||||
|
deal_address: The normalized deal address (lowercase, stripped)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if likely the same building, False otherwise
|
||||||
|
"""
|
||||||
|
if not search_address or not deal_address:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Exact match
|
||||||
|
if search_address == deal_address:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Extract key components for comparison
|
||||||
|
def extract_address_parts(addr: str) -> tuple:
|
||||||
|
"""Extract street name and number from address"""
|
||||||
|
# Remove common prefixes/suffixes and normalize
|
||||||
|
addr_clean = addr.replace('רח\'', '').replace('רחוב', '').replace('שד\'', '').replace('שדרות', '')
|
||||||
|
addr_clean = addr_clean.replace(' ', ' ').strip()
|
||||||
|
|
||||||
|
# Try to extract number and street name
|
||||||
|
parts = addr_clean.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
# Look for number (could be at start or end)
|
||||||
|
for i, part in enumerate(parts):
|
||||||
|
if part.isdigit() or any(c.isdigit() for c in part):
|
||||||
|
number = part
|
||||||
|
street_parts = parts[:i] + parts[i+1:]
|
||||||
|
street_name = ' '.join(street_parts).strip()
|
||||||
|
return (street_name, number)
|
||||||
|
|
||||||
|
return (addr_clean, '')
|
||||||
|
|
||||||
|
search_street, search_number = extract_address_parts(search_address)
|
||||||
|
deal_street, deal_number = extract_address_parts(deal_address)
|
||||||
|
|
||||||
|
# Same street and same number = same building
|
||||||
|
if (search_street and deal_street and search_number and deal_number and
|
||||||
|
search_street == deal_street and search_number == deal_number):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if one address is contained in the other (for different formats of same address)
|
||||||
|
if len(search_address) > 5 and len(deal_address) > 5:
|
||||||
|
if search_address in deal_address or deal_address in search_address:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
Reference in New Issue
Block a user