Add outlier filtering metadata to get_valuation_comparables response
The MCP now includes detailed outlier filtering information in the response,
allowing LLMs and bots to inform users about data quality improvements.
Changes to nadlan_mcp/fastmcp_server.py:
- Updated get_valuation_comparables to include outlier filtering metadata
- deal_breakdown now includes when filtering is applied:
- total_deals: Count after filtering (final result)
- total_deals_before_filtering: Original count before filtering
- outliers_removed: Number of deals filtered out
- filtering_method: Method used ("iqr", "percent", or "none")
- iqr_multiplier: IQR multiplier when using IQR method (e.g., 1.5)
- Updated docstring to clarify that outlier filtering is automatic
and metadata is included in response
Changes to tests/e2e/test_mcp_tools_comprehensive.py:
- Added assertions to verify outlier filtering metadata fields
- Test now validates the structure and types of filtering information
Example response:
{
"market_statistics": {
"deal_breakdown": {
"total_deals": 15,
"total_deals_before_filtering": 17,
"outliers_removed": 2,
"filtering_method": "iqr",
"iqr_multiplier": 1.5
}
}
}
This allows bots like nadlan-bot to properly inform users:
"Found 17 comparable deals, filtered 2 outliers using IQR method (k=1.5),
showing 15 deals."
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -776,8 +776,9 @@ def get_valuation_comparables(
|
|||||||
"""Get comparable properties for valuation analysis.
|
"""Get comparable properties for valuation analysis.
|
||||||
|
|
||||||
This tool provides detailed comparable deals filtered by your criteria.
|
This tool provides detailed comparable deals filtered by your criteria.
|
||||||
Returns a generous number of comparables by default - the LLM analyzing
|
Automatically applies IQR outlier filtering (k=1.5) to remove statistical outliers
|
||||||
the results can determine which are most similar based on the full details.
|
and improve data quality. The response includes metadata about filtering so you can
|
||||||
|
inform users about removed outliers.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
address: The address to find comparables for (in Hebrew or English)
|
address: The address to find comparables for (in Hebrew or English)
|
||||||
@@ -795,8 +796,14 @@ def get_valuation_comparables(
|
|||||||
max_comparables: Maximum number of deals to return (default: 50, optimized for MCP token limits)
|
max_comparables: Maximum number of deals to return (default: 50, optimized for MCP token limits)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
JSON string containing filtered comparable deals with full details.
|
JSON string containing:
|
||||||
Returns many comparables so LLM can assess similarity and relevance.
|
- Filtered comparable deals with full details
|
||||||
|
- deal_breakdown with outlier filtering metadata:
|
||||||
|
- total_deals: Count after filtering
|
||||||
|
- total_deals_before_filtering: Count before filtering
|
||||||
|
- outliers_removed: Number of deals filtered out
|
||||||
|
- filtering_method: Method used (e.g., "iqr")
|
||||||
|
- iqr_multiplier: IQR multiplier used (e.g., 1.5)
|
||||||
"""
|
"""
|
||||||
log_mcp_call(
|
log_mcp_call(
|
||||||
"get_valuation_comparables",
|
"get_valuation_comparables",
|
||||||
@@ -857,6 +864,7 @@ def get_valuation_comparables(
|
|||||||
|
|
||||||
# Apply outlier filtering to remove statistical outliers
|
# Apply outlier filtering to remove statistical outliers
|
||||||
config = get_config()
|
config = get_config()
|
||||||
|
outlier_report = None
|
||||||
if (
|
if (
|
||||||
config.analysis_outlier_method != "none"
|
config.analysis_outlier_method != "none"
|
||||||
and len(filtered_deals) >= config.analysis_min_deals_for_outlier_detection
|
and len(filtered_deals) >= config.analysis_min_deals_for_outlier_detection
|
||||||
@@ -868,6 +876,19 @@ def get_valuation_comparables(
|
|||||||
# Calculate statistics on filtered comparables
|
# Calculate statistics on filtered comparables
|
||||||
stats = client.calculate_deal_statistics(filtered_deals)
|
stats = client.calculate_deal_statistics(filtered_deals)
|
||||||
|
|
||||||
|
# Build deal breakdown with outlier filtering information
|
||||||
|
deal_breakdown = {
|
||||||
|
"total_deals": len(filtered_deals),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add outlier filtering metadata if filtering was applied
|
||||||
|
if outlier_report:
|
||||||
|
deal_breakdown["total_deals_before_filtering"] = outlier_report["total_deals"]
|
||||||
|
deal_breakdown["outliers_removed"] = outlier_report["outliers_removed"]
|
||||||
|
deal_breakdown["filtering_method"] = outlier_report["method_used"]
|
||||||
|
if outlier_report["method_used"] == "iqr":
|
||||||
|
deal_breakdown["iqr_multiplier"] = outlier_report["parameters"]["iqr_multiplier"]
|
||||||
|
|
||||||
# Normalize response structure to match other tools
|
# Normalize response structure to match other tools
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
{
|
{
|
||||||
@@ -885,9 +906,7 @@ def get_valuation_comparables(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"market_statistics": {
|
"market_statistics": {
|
||||||
"deal_breakdown": {
|
"deal_breakdown": deal_breakdown,
|
||||||
"total_deals": len(filtered_deals),
|
|
||||||
},
|
|
||||||
"price_statistics": stats.price_statistics,
|
"price_statistics": stats.price_statistics,
|
||||||
"area_statistics": stats.area_statistics,
|
"area_statistics": stats.area_statistics,
|
||||||
"price_per_sqm_statistics": stats.price_per_sqm_statistics,
|
"price_per_sqm_statistics": stats.price_per_sqm_statistics,
|
||||||
|
|||||||
@@ -92,12 +92,24 @@ class TestMCPToolsE2E:
|
|||||||
# Normalized structure: total_comparables -> market_statistics.deal_breakdown.total_deals
|
# Normalized structure: total_comparables -> market_statistics.deal_breakdown.total_deals
|
||||||
assert "market_statistics" in data
|
assert "market_statistics" in data
|
||||||
assert "deal_breakdown" in data["market_statistics"]
|
assert "deal_breakdown" in data["market_statistics"]
|
||||||
assert "total_deals" in data["market_statistics"]["deal_breakdown"]
|
deal_breakdown = data["market_statistics"]["deal_breakdown"]
|
||||||
assert isinstance(data["market_statistics"]["deal_breakdown"]["total_deals"], int)
|
assert "total_deals" in deal_breakdown
|
||||||
assert data["market_statistics"]["deal_breakdown"]["total_deals"] >= 0
|
assert isinstance(deal_breakdown["total_deals"], int)
|
||||||
|
assert deal_breakdown["total_deals"] >= 0
|
||||||
|
|
||||||
|
# Verify outlier filtering metadata is included when filtering is applied
|
||||||
|
if "outliers_removed" in deal_breakdown:
|
||||||
|
assert isinstance(deal_breakdown["outliers_removed"], int)
|
||||||
|
assert deal_breakdown["outliers_removed"] >= 0
|
||||||
|
assert "total_deals_before_filtering" in deal_breakdown
|
||||||
|
assert "filtering_method" in deal_breakdown
|
||||||
|
assert deal_breakdown["filtering_method"] in ["iqr", "percent", "none"]
|
||||||
|
if deal_breakdown["filtering_method"] == "iqr":
|
||||||
|
assert "iqr_multiplier" in deal_breakdown
|
||||||
|
assert isinstance(deal_breakdown["iqr_multiplier"], (int, float))
|
||||||
|
|
||||||
# Normalized structure: comparables -> deals
|
# Normalized structure: comparables -> deals
|
||||||
if data["market_statistics"]["deal_breakdown"]["total_deals"] > 0:
|
if deal_breakdown["total_deals"] > 0:
|
||||||
assert "deals" in data
|
assert "deals" in data
|
||||||
comp = data["deals"][0]
|
comp = data["deals"][0]
|
||||||
assert "deal_amount" in comp
|
assert "deal_amount" in comp
|
||||||
|
|||||||
Reference in New Issue
Block a user