Change default IQR multiplier to 1.0 for more aggressive outlier filtering

- Change ANALYSIS_IQR_MULTIPLIER default from 1.5 to 1.0 in config.py
- Add iqr_multiplier parameter to all filtering & statistics functions
- Allow runtime override via MCP tools (get_valuation_comparables, get_deal_statistics)
- Update CLAUDE.md docs with new default & override examples

Rationale: k=1.0 catches more suspicious deals (e.g. 43% below median) while
still preserving legitimate edge cases via hard bounds. Users can override
per-call for more conservative filtering (k=1.5) if needed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Nitzan Pomerantz
2025-11-28 22:40:29 +02:00
parent 1bc94e39ba
commit 8e591423da
6 changed files with 43 additions and 17 deletions
+8 -4
View File
@@ -252,7 +252,7 @@ The MCP now includes configurable outlier detection and robust statistical measu
```bash ```bash
# Outlier Detection Strategy # Outlier Detection Strategy
ANALYSIS_OUTLIER_METHOD=iqr # Options: iqr, percent, none (default: iqr) ANALYSIS_OUTLIER_METHOD=iqr # Options: iqr, percent, none (default: iqr)
ANALYSIS_IQR_MULTIPLIER=1.5 # 1.5=moderate, 3.0=conservative (default: 1.5) ANALYSIS_IQR_MULTIPLIER=1.0 # 1.0=aggressive, 1.5=moderate, 3.0=conservative (default: 1.0)
ANALYSIS_MIN_DEALS_FOR_OUTLIER_DETECTION=10 # Minimum deals needed (default: 10) ANALYSIS_MIN_DEALS_FOR_OUTLIER_DETECTION=10 # Minimum deals needed (default: 10)
# Hard Bounds (catches obvious data errors) # Hard Bounds (catches obvious data errors)
@@ -273,10 +273,14 @@ ANALYSIS_INCLUDE_UNFILTERED_STATS=true # Report both filtered/unfiltered (defau
from nadlan_mcp.govmap.statistics import calculate_deal_statistics from nadlan_mcp.govmap.statistics import calculate_deal_statistics
from nadlan_mcp.config import GovmapConfig from nadlan_mcp.config import GovmapConfig
# With outlier filtering (default behavior) # With outlier filtering (default behavior, k=1.0)
config = GovmapConfig() # Uses env vars or defaults config = GovmapConfig() # Uses env vars or defaults
stats = calculate_deal_statistics(deals, config) stats = calculate_deal_statistics(deals, config)
# Override IQR multiplier for more/less aggressive filtering
stats = calculate_deal_statistics(deals, config, iqr_multiplier=1.5) # More conservative
stats = calculate_deal_statistics(deals, config, iqr_multiplier=0.5) # More aggressive
# Access filtered statistics (main results after outlier removal) # Access filtered statistics (main results after outlier removal)
filtered_mean = stats.filtered_price_per_sqm_statistics["mean"] filtered_mean = stats.filtered_price_per_sqm_statistics["mean"]
filtered_median = stats.filtered_price_per_sqm_statistics["median"] filtered_median = stats.filtered_price_per_sqm_statistics["median"]
@@ -292,10 +296,10 @@ original_mean = stats.price_per_sqm_statistics["mean"]
``` ```
**Design Principles:** **Design Principles:**
- **Enabled by default**: Moderate filtering (k=1.5) to catch common errors - **Enabled by default**: Aggressive filtering (k=1.0) to catch common errors & suspicious deals
- **Transparent**: Both filtered and unfiltered statistics returned - **Transparent**: Both filtered and unfiltered statistics returned
- **Conservative with real data**: Hard bounds + IQR preserve legitimate high-end properties - **Conservative with real data**: Hard bounds + IQR preserve legitimate high-end properties
- **Configurable**: Adjust sensitivity via environment variables - **Configurable**: Adjust sensitivity via environment variables or function parameters
- **MCP provides data, LLM provides intelligence**: Outlier detection improves data quality; LLM interprets results - **MCP provides data, LLM provides intelligence**: Outlier detection improves data quality; LLM interprets results
### Retry Logic ### Retry Logic
+1 -1
View File
@@ -61,7 +61,7 @@ class GovmapConfig:
default_factory=lambda: os.getenv("ANALYSIS_OUTLIER_METHOD", "iqr") default_factory=lambda: os.getenv("ANALYSIS_OUTLIER_METHOD", "iqr")
) )
analysis_iqr_multiplier: float = field( analysis_iqr_multiplier: float = field(
default_factory=lambda: float(os.getenv("ANALYSIS_IQR_MULTIPLIER", "1.5")) default_factory=lambda: float(os.getenv("ANALYSIS_IQR_MULTIPLIER", "1.0"))
) )
analysis_min_deals_for_outlier_detection: int = field( analysis_min_deals_for_outlier_detection: int = field(
default_factory=lambda: int(os.getenv("ANALYSIS_MIN_DEALS_FOR_OUTLIER_DETECTION", "10")) default_factory=lambda: int(os.getenv("ANALYSIS_MIN_DEALS_FOR_OUTLIER_DETECTION", "10"))
+14 -5
View File
@@ -772,11 +772,12 @@ def get_valuation_comparables(
max_floor: Optional[int] = None, max_floor: Optional[int] = None,
radius_meters: int = 100, radius_meters: int = 100,
max_comparables: int = 50, max_comparables: int = 50,
iqr_multiplier: Optional[float] = None,
) -> str: ) -> str:
"""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.
Automatically applies IQR outlier filtering (k=1.5) to remove statistical outliers Automatically applies IQR outlier filtering (k=1.0 default) to remove statistical outliers
and improve data quality. The response includes metadata about filtering so you can and improve data quality. The response includes metadata about filtering so you can
inform users about removed outliers. inform users about removed outliers.
@@ -794,6 +795,7 @@ def get_valuation_comparables(
max_floor: Maximum floor number max_floor: Maximum floor number
radius_meters: Search radius in meters (default: 100, larger than find_recent_deals to get more comparables) radius_meters: Search radius in meters (default: 100, larger than find_recent_deals to get more 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)
iqr_multiplier: Override IQR multiplier for outlier detection (default: 1.0). Lower = more aggressive filtering
Returns: Returns:
JSON string containing: JSON string containing:
@@ -803,7 +805,7 @@ def get_valuation_comparables(
- total_deals_before_filtering: Count before filtering - total_deals_before_filtering: Count before filtering
- outliers_removed: Number of deals filtered out - outliers_removed: Number of deals filtered out
- filtering_method: Method used (e.g., "iqr") - filtering_method: Method used (e.g., "iqr")
- iqr_multiplier: IQR multiplier used (e.g., 1.5) - iqr_multiplier: IQR multiplier used (e.g., 1.0)
""" """
log_mcp_call( log_mcp_call(
"get_valuation_comparables", "get_valuation_comparables",
@@ -820,6 +822,7 @@ def get_valuation_comparables(
max_floor=max_floor, max_floor=max_floor,
radius_meters=radius_meters, radius_meters=radius_meters,
max_comparables=max_comparables, max_comparables=max_comparables,
iqr_multiplier=iqr_multiplier,
) )
try: try:
# Get all deals for the address with higher limits for valuation # Get all deals for the address with higher limits for valuation
@@ -876,10 +879,13 @@ def get_valuation_comparables(
): ):
deals_before_outlier_filter = len(filtered_deals) deals_before_outlier_filter = len(filtered_deals)
filtered_deals, outlier_report = filter_deals_for_analysis( filtered_deals, outlier_report = filter_deals_for_analysis(
filtered_deals, config, metric="price_per_sqm" filtered_deals, config, metric="price_per_sqm", iqr_multiplier=iqr_multiplier
)
effective_k = (
iqr_multiplier if iqr_multiplier is not None else config.analysis_iqr_multiplier
) )
logger.info( logger.info(
f"After outlier filtering ({config.analysis_outlier_method}, k={config.analysis_iqr_multiplier}): " f"After outlier filtering ({config.analysis_outlier_method}, k={effective_k}): "
f"{len(filtered_deals)} deals (removed {deals_before_outlier_filter - len(filtered_deals)} outliers)" f"{len(filtered_deals)} deals (removed {deals_before_outlier_filter - len(filtered_deals)} outliers)"
) )
else: else:
@@ -946,6 +952,7 @@ def get_deal_statistics(
property_type: Optional[str] = None, property_type: Optional[str] = None,
min_rooms: Optional[float] = None, min_rooms: Optional[float] = None,
max_rooms: Optional[float] = None, max_rooms: Optional[float] = None,
iqr_multiplier: Optional[float] = None,
) -> str: ) -> str:
"""Calculate statistical aggregations on deal data for an address. """Calculate statistical aggregations on deal data for an address.
@@ -958,6 +965,7 @@ def get_deal_statistics(
property_type: Filter by property type (e.g., "דירה", "בית") property_type: Filter by property type (e.g., "דירה", "בית")
min_rooms: Minimum number of rooms min_rooms: Minimum number of rooms
max_rooms: Maximum number of rooms max_rooms: Maximum number of rooms
iqr_multiplier: Override IQR multiplier for outlier detection (default: 1.0). Lower = more aggressive filtering
Returns: Returns:
JSON string containing statistical metrics (mean, median, percentiles, etc.) JSON string containing statistical metrics (mean, median, percentiles, etc.)
@@ -969,6 +977,7 @@ def get_deal_statistics(
property_type=property_type, property_type=property_type,
min_rooms=min_rooms, min_rooms=min_rooms,
max_rooms=max_rooms, max_rooms=max_rooms,
iqr_multiplier=iqr_multiplier,
) )
try: try:
# Get all deals for the address # Get all deals for the address
@@ -998,7 +1007,7 @@ def get_deal_statistics(
) )
# Calculate statistics # Calculate statistics
stats = client.calculate_deal_statistics(deals) stats = client.calculate_deal_statistics(deals, iqr_multiplier=iqr_multiplier)
# Normalize response structure to match other tools # Normalize response structure to match other tools
return json.dumps( return json.dumps(
+5 -2
View File
@@ -873,7 +873,9 @@ class GovmapClient:
) )
# Statistics methods (delegate to statistics module) # Statistics methods (delegate to statistics module)
def calculate_deal_statistics(self, deals: List[Deal]) -> DealStatistics: def calculate_deal_statistics(
self, deals: List[Deal], iqr_multiplier: Optional[float] = None
) -> DealStatistics:
""" """
Calculate statistical aggregations on deal data. Calculate statistical aggregations on deal data.
@@ -881,11 +883,12 @@ class GovmapClient:
Args: Args:
deals: List of Deal model instances deals: List of Deal model instances
iqr_multiplier: Override IQR multiplier for outlier detection (optional)
Returns: Returns:
DealStatistics model with comprehensive metrics DealStatistics model with comprehensive metrics
""" """
return statistics.calculate_deal_statistics(deals) return statistics.calculate_deal_statistics(deals, iqr_multiplier=iqr_multiplier)
def _calculate_std_dev(self, values: List[float]) -> float: def _calculate_std_dev(self, values: List[float]) -> float:
""" """
+12 -3
View File
@@ -173,7 +173,10 @@ def apply_hard_bounds_deal_amount(
def filter_deals_for_analysis( def filter_deals_for_analysis(
deals: List[Deal], config: Optional[GovmapConfig] = None, metric: str = "price_per_sqm" deals: List[Deal],
config: Optional[GovmapConfig] = None,
metric: str = "price_per_sqm",
iqr_multiplier: Optional[float] = None,
) -> Tuple[List[Deal], Dict[str, Any]]: ) -> Tuple[List[Deal], Dict[str, Any]]:
""" """
Filter deals to remove outliers based on configuration. Filter deals to remove outliers based on configuration.
@@ -192,6 +195,7 @@ def filter_deals_for_analysis(
config: Configuration object (optional, uses global if not provided) config: Configuration object (optional, uses global if not provided)
metric: Which metric to apply statistical outlier detection to metric: Which metric to apply statistical outlier detection to
Options: "price_per_sqm", "deal_amount" Options: "price_per_sqm", "deal_amount"
iqr_multiplier: Override IQR multiplier (optional, uses config value if not provided)
Returns: Returns:
Tuple of: Tuple of:
@@ -239,6 +243,11 @@ def filter_deals_for_analysis(
filters_to_remove[i] = True filters_to_remove[i] = True
# Step 3: Apply statistical outlier detection to specified metric # Step 3: Apply statistical outlier detection to specified metric
# Use override value if provided, otherwise use config
effective_iqr_multiplier = (
iqr_multiplier if iqr_multiplier is not None else config.analysis_iqr_multiplier
)
if config.analysis_outlier_method == "iqr": if config.analysis_outlier_method == "iqr":
# Extract values for the specified metric # Extract values for the specified metric
if metric == "price_per_sqm": if metric == "price_per_sqm":
@@ -260,7 +269,7 @@ def filter_deals_for_analysis(
value_indices = [] value_indices = []
if values: if values:
statistical_outliers = detect_outliers_iqr(values, config.analysis_iqr_multiplier) statistical_outliers = detect_outliers_iqr(values, effective_iqr_multiplier)
for i, is_outlier in enumerate(statistical_outliers): for i, is_outlier in enumerate(statistical_outliers):
if is_outlier: if is_outlier:
filters_to_remove[value_indices[i]] = True filters_to_remove[value_indices[i]] = True
@@ -302,7 +311,7 @@ def filter_deals_for_analysis(
"outlier_indices": outlier_indices, "outlier_indices": outlier_indices,
"method_used": config.analysis_outlier_method, "method_used": config.analysis_outlier_method,
"parameters": { "parameters": {
"iqr_multiplier": config.analysis_iqr_multiplier "iqr_multiplier": effective_iqr_multiplier
if config.analysis_outlier_method == "iqr" if config.analysis_outlier_method == "iqr"
else None, else None,
"metric": metric, "metric": metric,
+3 -2
View File
@@ -152,7 +152,7 @@ def _calculate_basic_stats(deals: List[Deal]) -> Dict:
def calculate_deal_statistics( def calculate_deal_statistics(
deals: List[Deal], config: Optional[GovmapConfig] = None deals: List[Deal], config: Optional[GovmapConfig] = None, iqr_multiplier: Optional[float] = None
) -> DealStatistics: ) -> DealStatistics:
""" """
Calculate statistical aggregations on deal data with optional outlier filtering. Calculate statistical aggregations on deal data with optional outlier filtering.
@@ -164,6 +164,7 @@ def calculate_deal_statistics(
Args: Args:
deals: List of Deal model instances deals: List of Deal model instances
config: Configuration object (optional, uses global config if not provided) config: Configuration object (optional, uses global config if not provided)
iqr_multiplier: Override IQR multiplier (optional, uses config value if not provided)
Returns: Returns:
DealStatistics model with comprehensive metrics, including: DealStatistics model with comprehensive metrics, including:
@@ -203,7 +204,7 @@ def calculate_deal_statistics(
): ):
# Filter deals for analysis (primarily targeting price_per_sqm outliers) # Filter deals for analysis (primarily targeting price_per_sqm outliers)
filtered_deals, report_dict = filter_deals_for_analysis( filtered_deals, report_dict = filter_deals_for_analysis(
deals, config, metric="price_per_sqm" deals, config, metric="price_per_sqm", iqr_multiplier=iqr_multiplier
) )
# Create OutlierReport model # Create OutlierReport model