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:
@@ -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
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user