This commit is contained in:
Nitzan Pomerantz
2025-10-30 18:26:48 +02:00
parent ae7f7f51c5
commit 04a1e68604
13 changed files with 1870 additions and 2 deletions
+307
View File
@@ -0,0 +1,307 @@
"""
Tests for nadlan_mcp.govmap.filters module.
Comprehensive tests for filter_deals_by_criteria function.
"""
import pytest
from nadlan_mcp.govmap.filters import filter_deals_by_criteria
from nadlan_mcp.govmap.models import Deal, DealFilters
class TestFilterDealsByCriteria:
"""Test cases for filter_deals_by_criteria function."""
@pytest.fixture
def sample_deals(self):
"""Create sample deals for testing."""
return [
Deal(
objectid=1,
deal_amount=1000000.0,
deal_date="2024-01-15",
property_type_description="דירה",
rooms=3.0,
asset_area=80.0,
floor_number=2,
),
Deal(
objectid=2,
deal_amount=1500000.0,
deal_date="2024-02-01",
property_type_description="דירת גג",
rooms=4.0,
asset_area=100.0,
floor_number=5,
),
Deal(
objectid=3,
deal_amount=2000000.0,
deal_date="2024-02-15",
property_type_description="בית פרטי",
rooms=5.0,
asset_area=150.0,
floor_number=0, # Ground floor
),
Deal(
objectid=4,
deal_amount=800000.0,
deal_date="2024-03-01",
property_type_description="דירה",
rooms=2.0,
asset_area=60.0,
floor_number=1,
),
Deal(
objectid=5,
deal_amount=1200000.0,
deal_date="2024-03-15",
property_type_description="פנטהאוז",
rooms=4.5,
asset_area=120.0,
floor_number=10,
),
]
def test_no_filters_returns_all_deals(self, sample_deals):
"""Test that with no filters, all deals are returned."""
result = filter_deals_by_criteria(sample_deals)
assert len(result) == 5
assert result == sample_deals
def test_filter_by_property_type_exact_match(self, sample_deals):
"""Test filtering by exact property type."""
result = filter_deals_by_criteria(sample_deals, property_type="בית פרטי")
assert len(result) == 1
assert result[0].objectid == 3
def test_filter_by_property_type_partial_match(self, sample_deals):
"""Test filtering by partial property type (substring)."""
result = filter_deals_by_criteria(sample_deals, property_type="דירה")
# Should match only "דירה" exactly (not "דירת גג" because "דירה" != "דירת")
assert len(result) == 2
assert set(d.objectid for d in result) == {1, 4}
def test_filter_by_property_type_case_insensitive(self, sample_deals):
"""Test that property type filtering is case-insensitive."""
result = filter_deals_by_criteria(sample_deals, property_type="דירה")
result_upper = filter_deals_by_criteria(sample_deals, property_type="דירה")
assert len(result) == len(result_upper)
def test_filter_by_min_rooms(self, sample_deals):
"""Test filtering by minimum rooms."""
result = filter_deals_by_criteria(sample_deals, min_rooms=4.0)
assert len(result) == 3
assert set(d.objectid for d in result) == {2, 3, 5}
def test_filter_by_max_rooms(self, sample_deals):
"""Test filtering by maximum rooms."""
result = filter_deals_by_criteria(sample_deals, max_rooms=3.0)
assert len(result) == 2
assert set(d.objectid for d in result) == {1, 4}
def test_filter_by_room_range(self, sample_deals):
"""Test filtering by room range."""
result = filter_deals_by_criteria(sample_deals, min_rooms=3.0, max_rooms=4.0)
assert len(result) == 2
assert set(d.objectid for d in result) == {1, 2}
def test_filter_by_min_price(self, sample_deals):
"""Test filtering by minimum price."""
result = filter_deals_by_criteria(sample_deals, min_price=1200000.0)
assert len(result) == 3
assert set(d.objectid for d in result) == {2, 3, 5}
def test_filter_by_max_price(self, sample_deals):
"""Test filtering by maximum price."""
result = filter_deals_by_criteria(sample_deals, max_price=1000000.0)
assert len(result) == 2
assert set(d.objectid for d in result) == {1, 4}
def test_filter_by_price_range(self, sample_deals):
"""Test filtering by price range."""
result = filter_deals_by_criteria(
sample_deals, min_price=1000000.0, max_price=1500000.0
)
assert len(result) == 3
assert set(d.objectid for d in result) == {1, 2, 5}
def test_filter_by_min_area(self, sample_deals):
"""Test filtering by minimum area."""
result = filter_deals_by_criteria(sample_deals, min_area=100.0)
assert len(result) == 3
assert set(d.objectid for d in result) == {2, 3, 5}
def test_filter_by_max_area(self, sample_deals):
"""Test filtering by maximum area."""
result = filter_deals_by_criteria(sample_deals, max_area=80.0)
assert len(result) == 2
assert set(d.objectid for d in result) == {1, 4}
def test_filter_by_area_range(self, sample_deals):
"""Test filtering by area range."""
result = filter_deals_by_criteria(
sample_deals, min_area=80.0, max_area=120.0
)
assert len(result) == 3
assert set(d.objectid for d in result) == {1, 2, 5}
def test_filter_by_min_floor(self, sample_deals):
"""Test filtering by minimum floor."""
result = filter_deals_by_criteria(sample_deals, min_floor=2)
assert len(result) == 3
assert set(d.objectid for d in result) == {1, 2, 5}
def test_filter_by_max_floor(self, sample_deals):
"""Test filtering by maximum floor."""
result = filter_deals_by_criteria(sample_deals, max_floor=2)
assert len(result) == 3
assert set(d.objectid for d in result) == {1, 3, 4}
def test_filter_by_floor_range(self, sample_deals):
"""Test filtering by floor range."""
result = filter_deals_by_criteria(sample_deals, min_floor=1, max_floor=5)
assert len(result) == 3
assert set(d.objectid for d in result) == {1, 2, 4}
def test_combined_filters(self, sample_deals):
"""Test combining multiple filters."""
result = filter_deals_by_criteria(
sample_deals,
property_type="דירה",
min_rooms=3.0,
min_price=1000000.0,
max_price=1500000.0,
)
# Should match only objectid=1 (דירה with 3 rooms, price 1M)
# objectid=2 is "דירת גג" not "דירה", so filtered out
assert len(result) == 1
assert set(d.objectid for d in result) == {1}
def test_filter_using_dealfilters_model(self, sample_deals):
"""Test filtering using DealFilters model."""
filters = DealFilters(
property_type="דירה", min_rooms=3.0, max_rooms=4.0
)
result = filter_deals_by_criteria(sample_deals, filters=filters)
# Should match only objectid=1 (דירה with 3 rooms)
# objectid=2 is "דירת גג" not exact match
assert len(result) == 1
assert set(d.objectid for d in result) == {1}
def test_filter_using_dict(self, sample_deals):
"""Test filtering using dict (converted to DealFilters)."""
filters_dict = {
"property_type": "דירה",
"min_rooms": 3.0,
"max_rooms": 4.0,
}
result = filter_deals_by_criteria(sample_deals, filters=filters_dict)
# Should match only objectid=1
assert len(result) == 1
assert set(d.objectid for d in result) == {1}
def test_individual_params_override_filters_model(self, sample_deals):
"""Test that individual parameters override filters model."""
filters = DealFilters(min_rooms=5.0) # Would match only objectid=3
result = filter_deals_by_criteria(
sample_deals, filters=filters, min_rooms=3.0 # Override to 3.0
)
# Should use min_rooms=3.0, not 5.0
assert len(result) == 4
assert set(d.objectid for d in result) == {1, 2, 3, 5}
def test_filters_exclude_deals_with_missing_property_type(self):
"""Test that deals with missing property_type are excluded when filter active."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date="2024-01-01", property_type_description="דירה"),
Deal(objectid=2, deal_amount=1000000, deal_date="2024-01-01", property_type_description=None),
Deal(objectid=3, deal_amount=1000000, deal_date="2024-01-01"), # No property_type
]
result = filter_deals_by_criteria(deals, property_type="דירה")
assert len(result) == 1
assert result[0].objectid == 1
def test_filters_exclude_deals_with_missing_rooms(self):
"""Test that deals with missing rooms are excluded when filter active."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date="2024-01-01", rooms=3.0),
Deal(objectid=2, deal_amount=1000000, deal_date="2024-01-01", rooms=None),
Deal(objectid=3, deal_amount=1000000, deal_date="2024-01-01"), # No rooms
]
result = filter_deals_by_criteria(deals, min_rooms=2.0)
assert len(result) == 1
assert result[0].objectid == 1
def test_filters_exclude_deals_with_missing_area(self):
"""Test that deals with missing area are excluded when filter active."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date="2024-01-01", asset_area=80.0),
Deal(objectid=2, deal_amount=1000000, deal_date="2024-01-01", asset_area=None),
Deal(objectid=3, deal_amount=1000000, deal_date="2024-01-01"), # No area
]
result = filter_deals_by_criteria(deals, min_area=50.0)
assert len(result) == 1
assert result[0].objectid == 1
def test_empty_deals_list_returns_empty(self):
"""Test filtering empty list returns empty list."""
result = filter_deals_by_criteria([], property_type="דירה")
assert result == []
def test_no_matches_returns_empty(self, sample_deals):
"""Test filtering with no matches returns empty list."""
result = filter_deals_by_criteria(sample_deals, min_rooms=10.0)
assert len(result) == 0
def test_invalid_input_raises_error(self):
"""Test that invalid input raises ValueError."""
with pytest.raises(ValueError, match="deals must be a list"):
filter_deals_by_criteria("not a list")
def test_invalid_room_range_raises_error(self, sample_deals):
"""Test that min_rooms > max_rooms raises error."""
with pytest.raises(ValueError, match="min_rooms cannot be greater than max_rooms"):
filter_deals_by_criteria(sample_deals, min_rooms=5.0, max_rooms=3.0)
def test_invalid_price_range_raises_error(self, sample_deals):
"""Test that min_price > max_price raises error."""
with pytest.raises(ValueError, match="min_price cannot be greater than max_price"):
filter_deals_by_criteria(sample_deals, min_price=2000000, max_price=1000000)
def test_invalid_area_range_raises_error(self, sample_deals):
"""Test that min_area > max_area raises error."""
with pytest.raises(ValueError, match="min_area cannot be greater than max_area"):
filter_deals_by_criteria(sample_deals, min_area=150.0, max_area=80.0)
def test_invalid_floor_range_raises_error(self, sample_deals):
"""Test that min_floor > max_floor raises error."""
with pytest.raises(ValueError, match="min_floor cannot be greater than max_floor"):
filter_deals_by_criteria(sample_deals, min_floor=5, max_floor=2)
def test_floor_extraction_from_floor_description(self):
"""Test floor filtering with Hebrew floor descriptions."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date="2024-01-01", floor="קרקע"), # Ground=0
Deal(objectid=2, deal_amount=1000000, deal_date="2024-01-01", floor="קומה 3"), # Floor 3
Deal(objectid=3, deal_amount=1000000, deal_date="2024-01-01", floor="מרתף"), # Basement=-1
]
result = filter_deals_by_criteria(deals, min_floor=0)
# Should match ground (0) and floor 3, but not basement (-1)
assert len(result) == 2
assert set(d.objectid for d in result) == {1, 2}
@pytest.mark.parametrize(
"min_val,max_val,expected_count",
[
(None, None, 5), # No filter
(3.0, 4.0, 2), # Range
(3.0, None, 4), # Min only
(None, 4.0, 3), # Max only
(10.0, 20.0, 0), # No matches
],
)
def test_room_filtering_parametrized(self, sample_deals, min_val, max_val, expected_count):
"""Parametrized test for room filtering."""
result = filter_deals_by_criteria(sample_deals, min_rooms=min_val, max_rooms=max_val)
assert len(result) == expected_count
+427
View File
@@ -0,0 +1,427 @@
"""
Tests for nadlan_mcp.govmap.market_analysis module.
Comprehensive tests for market analysis functions.
"""
import pytest
from datetime import date, datetime, timedelta
from nadlan_mcp.govmap.market_analysis import (
parse_deal_dates,
calculate_market_activity_score,
analyze_investment_potential,
get_market_liquidity,
)
from nadlan_mcp.govmap.models import Deal, MarketActivityScore, InvestmentAnalysis, LiquidityMetrics
def get_recent_date(months_ago=0, days_ago=0):
"""Helper to get recent dates for testing."""
return (datetime.now() - timedelta(days=months_ago * 30 + days_ago)).date()
class TestParseDealDates:
"""Test cases for parse_deal_dates helper function."""
@pytest.fixture
def sample_deals(self):
"""Create sample deals spanning multiple months (recent dates)."""
return [
Deal(objectid=1, deal_amount=1000000, deal_date=get_recent_date(months_ago=4, days_ago=15), asset_area=80),
Deal(objectid=2, deal_amount=1100000, deal_date=get_recent_date(months_ago=4, days_ago=10), asset_area=85),
Deal(objectid=3, deal_amount=1200000, deal_date=get_recent_date(months_ago=3, days_ago=20), asset_area=90),
Deal(objectid=4, deal_amount=1300000, deal_date=get_recent_date(months_ago=2, days_ago=25), asset_area=95),
Deal(objectid=5, deal_amount=1400000, deal_date=get_recent_date(months_ago=1, days_ago=18), asset_area=100),
]
def test_parse_deal_dates_basic(self, sample_deals):
"""Test basic date parsing functionality."""
deal_dates, monthly, quarterly = parse_deal_dates(sample_deals)
assert len(deal_dates) == 5
assert len(monthly) >= 4 # At least 4 different months
assert len(quarterly) >= 1 # At least 1 quarter
def test_parse_deal_dates_quarterly_grouping(self, sample_deals):
"""Test quarterly grouping."""
_, _, quarterly = parse_deal_dates(sample_deals)
assert len(quarterly) >= 1 # At least one quarter represented
def test_parse_deal_dates_with_time_filter(self, sample_deals):
"""Test filtering by time period."""
# Filter to last 6 months (should include all our recent sample deals)
deal_dates, monthly, _ = parse_deal_dates(sample_deals, time_period_months=6)
# All sample deals are within last 5 months, so should all be included
assert len(deal_dates) == 5
def test_parse_deal_dates_with_date_objects(self):
"""Test parsing with date objects instead of strings."""
recent = datetime.now().date()
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date=recent - timedelta(days=30), asset_area=80),
Deal(objectid=2, deal_amount=1100000, deal_date=recent - timedelta(days=60), asset_area=85),
]
deal_dates, monthly, _ = parse_deal_dates(deals)
assert len(deal_dates) == 2
assert len(monthly) >= 1
def test_parse_deal_dates_all_valid(self):
"""Test that all valid dates are parsed."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date=get_recent_date(months_ago=1), asset_area=80),
Deal(objectid=2, deal_amount=1100000, deal_date=get_recent_date(months_ago=2), asset_area=85),
]
deal_dates, _, _ = parse_deal_dates(deals)
assert len(deal_dates) == 2
def test_parse_deal_dates_empty_list_raises_error(self):
"""Test that empty list raises error."""
with pytest.raises(ValueError, match="No valid deal dates"):
parse_deal_dates([])
class TestCalculateMarketActivityScore:
"""Test cases for calculate_market_activity_score function."""
def test_market_activity_basic(self):
"""Test basic market activity calculation."""
# Create 12 deals spread across last 12 months
deals = [
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=i), asset_area=80)
for i in range(12)
]
score = calculate_market_activity_score(deals, time_period_months=12)
assert isinstance(score, MarketActivityScore)
assert score.total_deals == 12
assert 0.9 <= score.deals_per_month <= 1.2 # Approximately 1 deal/month
assert score.time_period_months == 12
assert len(score.monthly_distribution) > 0
def test_market_activity_high_volume(self):
"""Test activity score with high volume."""
# Create 120 deals spread across last 10 months = ~12 deals/month (very high)
deals = []
for i in range(120):
month_ago = (i % 10)
day = (i % 28) + 1
deals.append(
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=month_ago, days_ago=day), asset_area=80)
)
score = calculate_market_activity_score(deals, time_period_months=12)
assert score.activity_score == 100.0 # Should max out at 100
def test_market_activity_low_volume(self):
"""Test activity score with low volume."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date=get_recent_date(months_ago=1), asset_area=80),
Deal(objectid=2, deal_amount=1100000, deal_date=get_recent_date(months_ago=6), asset_area=85),
]
score = calculate_market_activity_score(deals, time_period_months=12)
assert score.total_deals == 2
assert score.activity_score < 50 # Low activity
def test_market_activity_trend_increasing(self):
"""Test trend detection - increasing activity."""
# More deals in recent months (month 0 is most recent)
deals = []
for i in range(12):
months_ago = 11 - i # Start from 11 months ago
num_deals = i + 1 # Increasing: 1 deal earliest, 12 deals most recent
for j in range(num_deals):
deals.append(
Deal(objectid=len(deals), deal_amount=1000000, deal_date=get_recent_date(months_ago=months_ago, days_ago=j), asset_area=80)
)
score = calculate_market_activity_score(deals, time_period_months=12)
assert score.trend == "increasing"
def test_market_activity_trend_decreasing(self):
"""Test trend detection - decreasing activity."""
# Fewer deals in recent months
deals = []
for i in range(12):
months_ago = 11 - i # Start from 11 months ago
num_deals = 12 - i # Decreasing: 12 deals earliest, 1 deal most recent
for j in range(num_deals):
deals.append(
Deal(objectid=len(deals), deal_amount=1000000, deal_date=get_recent_date(months_ago=months_ago, days_ago=j), asset_area=80)
)
score = calculate_market_activity_score(deals, time_period_months=12)
assert score.trend == "decreasing"
def test_market_activity_trend_stable(self):
"""Test trend detection - stable activity."""
# Same number of deals each month
deals = [
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=i), asset_area=80)
for i in range(12)
]
score = calculate_market_activity_score(deals, time_period_months=12)
# With evenly distributed deals, trend could be stable or slightly increasing
assert score.trend in ["stable", "increasing"]
def test_market_activity_insufficient_data_for_trend(self):
"""Test trend with insufficient data."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date=get_recent_date(months_ago=1), asset_area=80),
Deal(objectid=2, deal_amount=1100000, deal_date=get_recent_date(months_ago=2), asset_area=85),
]
score = calculate_market_activity_score(deals, time_period_months=12)
assert score.trend == "insufficient_data"
def test_market_activity_empty_raises_error(self):
"""Test that empty deals list raises error."""
with pytest.raises(ValueError, match="Cannot calculate market activity"):
calculate_market_activity_score([])
@pytest.mark.parametrize(
"num_deals,months,expected_dpm_range",
[
(10, 10, (0.8, 1.2)), # ~1 deal/month
(50, 10, (4.5, 5.5)), # ~5 deals/month
(100, 10, (9.5, 10.5)), # ~10 deals/month
],
)
def test_market_activity_deals_per_month(self, num_deals, months, expected_dpm_range):
"""Parametrized test for deals per month calculation."""
deals = []
for i in range(num_deals):
month_ago = i % months
day = (i // months) % 28
deals.append(
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=month_ago, days_ago=day), asset_area=80)
)
score = calculate_market_activity_score(deals, time_period_months=12)
assert expected_dpm_range[0] <= score.deals_per_month <= expected_dpm_range[1]
class TestAnalyzeInvestmentPotential:
"""Test cases for analyze_investment_potential function."""
def test_investment_analysis_basic(self):
"""Test basic investment analysis."""
# Create deals with increasing prices
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date="2024-01-01", asset_area=100), # 10000/sqm
Deal(objectid=2, deal_amount=1100000, deal_date="2024-02-01", asset_area=100), # 11000/sqm
Deal(objectid=3, deal_amount=1200000, deal_date="2024-03-01", asset_area=100), # 12000/sqm
]
analysis = analyze_investment_potential(deals)
assert isinstance(analysis, InvestmentAnalysis)
assert analysis.total_deals == 3
assert analysis.price_trend == "increasing"
assert analysis.price_appreciation_rate > 0
assert 0 <= analysis.investment_score <= 100
def test_investment_analysis_declining_prices(self):
"""Test investment analysis with declining prices."""
deals = [
Deal(objectid=1, deal_amount=1200000, deal_date="2024-01-01", asset_area=100),
Deal(objectid=2, deal_amount=1100000, deal_date="2024-02-01", asset_area=100),
Deal(objectid=3, deal_amount=1000000, deal_date="2024-03-01", asset_area=100),
]
analysis = analyze_investment_potential(deals)
assert analysis.price_trend == "decreasing"
assert analysis.price_appreciation_rate < 0
assert analysis.price_change_pct < 0
def test_investment_analysis_stable_prices(self):
"""Test investment analysis with stable prices."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date="2024-01-01", asset_area=100),
Deal(objectid=2, deal_amount=1005000, deal_date="2024-02-01", asset_area=100),
Deal(objectid=3, deal_amount=1000000, deal_date="2024-03-01", asset_area=100),
]
analysis = analyze_investment_potential(deals)
assert analysis.price_trend == "stable"
assert -2 <= analysis.price_appreciation_rate <= 2
def test_investment_analysis_volatility_low(self):
"""Test volatility calculation with low volatility."""
# Prices very similar
deals = [
Deal(objectid=i, deal_amount=1000000 + i*1000, deal_date=f"2024-{i+1:02d}-01", asset_area=100)
for i in range(5)
]
analysis = analyze_investment_potential(deals)
assert analysis.market_stability in ["very_stable", "stable"]
assert analysis.price_volatility < 50
def test_investment_analysis_volatility_high(self):
"""Test volatility calculation with high volatility."""
# Prices vary wildly
deals = [
Deal(objectid=1, deal_amount=800000, deal_date="2024-01-01", asset_area=100),
Deal(objectid=2, deal_amount=1500000, deal_date="2024-02-01", asset_area=100),
Deal(objectid=3, deal_amount=900000, deal_date="2024-03-01", asset_area=100),
Deal(objectid=4, deal_amount=1400000, deal_date="2024-04-01", asset_area=100),
]
analysis = analyze_investment_potential(deals)
assert analysis.market_stability in ["volatile", "very_volatile", "moderate"]
assert analysis.price_volatility > 20
def test_investment_analysis_data_quality_excellent(self):
"""Test data quality assessment with excellent data."""
deals = [
Deal(objectid=i, deal_amount=1000000, deal_date=f"2024-{(i%12)+1:02d}-01", asset_area=100)
for i in range(25)
]
analysis = analyze_investment_potential(deals)
assert analysis.data_quality == "excellent"
assert analysis.total_deals >= 20
def test_investment_analysis_data_quality_limited(self):
"""Test data quality assessment with limited data."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date="2024-01-01", asset_area=100),
Deal(objectid=2, deal_amount=1100000, deal_date="2024-02-01", asset_area=100),
Deal(objectid=3, deal_amount=1200000, deal_date="2024-03-01", asset_area=100),
]
analysis = analyze_investment_potential(deals)
assert analysis.data_quality == "limited"
assert analysis.total_deals < 5
def test_investment_analysis_insufficient_data_raises_error(self):
"""Test that insufficient data raises error."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date="2024-01-01", asset_area=100),
Deal(objectid=2, deal_amount=1100000, deal_date="2024-02-01", asset_area=100),
]
with pytest.raises(ValueError, match="Insufficient data"):
analyze_investment_potential(deals)
def test_investment_analysis_empty_raises_error(self):
"""Test that empty deals list raises error."""
with pytest.raises(ValueError, match="Cannot analyze investment"):
analyze_investment_potential([])
def test_investment_analysis_no_price_per_sqm_raises_error(self):
"""Test that deals without price_per_sqm raise error."""
# Deals without asset_area won't have price_per_sqm
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date="2024-01-01"),
Deal(objectid=2, deal_amount=1100000, deal_date="2024-02-01"),
Deal(objectid=3, deal_amount=1200000, deal_date="2024-03-01"),
]
with pytest.raises(ValueError, match="Insufficient data"):
analyze_investment_potential(deals)
@pytest.mark.parametrize(
"price_changes,expected_trend",
[
([1000000, 1100000, 1200000], "increasing"), # +20%
([1200000, 1100000, 1000000], "decreasing"), # -20%
([1000000, 1010000, 1000000], "stable"), # <2% change
],
)
def test_investment_analysis_price_trends(self, price_changes, expected_trend):
"""Parametrized test for price trend detection."""
deals = [
Deal(objectid=i, deal_amount=price, deal_date=f"2024-{i+1:02d}-01", asset_area=100)
for i, price in enumerate(price_changes)
]
analysis = analyze_investment_potential(deals)
assert analysis.price_trend == expected_trend
class TestGetMarketLiquidity:
"""Test cases for get_market_liquidity function."""
def test_market_liquidity_basic(self):
"""Test basic liquidity calculation."""
deals = [
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=i), asset_area=80)
for i in range(12)
]
liquidity = get_market_liquidity(deals, time_period_months=12)
assert isinstance(liquidity, LiquidityMetrics)
assert liquidity.total_deals == 12
assert 0.9 <= liquidity.avg_deals_per_month <= 1.2 # Approximately 1
assert liquidity.time_period_months == 12
assert liquidity.liquidity_score >= 0
def test_market_liquidity_very_high(self):
"""Test very high liquidity."""
# 100 deals spread across last 10 months = ~10/month
deals = []
for i in range(100):
month_ago = i % 10
day = (i % 28) + 1
deals.append(
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=month_ago, days_ago=day), asset_area=80)
)
liquidity = get_market_liquidity(deals, time_period_months=12)
assert liquidity.market_activity_level == "very_high"
assert liquidity.liquidity_score == 100.0
def test_market_liquidity_low(self):
"""Test low liquidity."""
deals = [
Deal(objectid=1, deal_amount=1000000, deal_date=get_recent_date(months_ago=1), asset_area=80),
Deal(objectid=2, deal_amount=1100000, deal_date=get_recent_date(months_ago=6), asset_area=85),
]
liquidity = get_market_liquidity(deals, time_period_months=12)
assert liquidity.market_activity_level in ["very_low", "low"]
assert liquidity.liquidity_score < 50
def test_market_liquidity_deal_velocity(self):
"""Test deal velocity calculation."""
# 12 deals spread evenly across 12 months
deals = [
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=i), asset_area=80)
for i in range(12)
]
liquidity = get_market_liquidity(deals, time_period_months=12)
# deal_velocity should equal avg_deals_per_month
assert liquidity.deal_velocity == liquidity.avg_deals_per_month
assert 0.9 <= liquidity.deal_velocity <= 1.2
def test_market_liquidity_empty_raises_error(self):
"""Test that empty deals list raises error."""
with pytest.raises(ValueError, match="Cannot calculate market liquidity"):
get_market_liquidity([])
@pytest.mark.parametrize(
"num_deals,months,expected_ratings",
[
(100, 10, ["very_high"]), # 10 deals/month
(60, 10, ["high"]), # 6 deals/month
(30, 10, ["moderate"]), # 3 deals/month
(5, 10, ["low"]), # 0.5 deals/month
(2, 10, ["low", "very_low"]), # 0.2 deals/month - edge case
],
)
def test_market_liquidity_ratings(self, num_deals, months, expected_ratings):
"""Parametrized test for liquidity ratings."""
deals = []
for i in range(num_deals):
month_ago = i % months
day = (i // months) % 28
deals.append(
Deal(objectid=i, deal_amount=1000000, deal_date=get_recent_date(months_ago=month_ago, days_ago=day), asset_area=80)
)
liquidity = get_market_liquidity(deals, time_period_months=12)
assert liquidity.market_activity_level in expected_ratings
+355
View File
@@ -0,0 +1,355 @@
"""
Tests for nadlan_mcp.govmap.statistics module.
Comprehensive tests for statistical calculation functions.
"""
import pytest
from datetime import date
from nadlan_mcp.govmap.statistics import calculate_deal_statistics, calculate_std_dev
from nadlan_mcp.govmap.models import Deal, DealStatistics
class TestCalculateDealStatistics:
"""Test cases for calculate_deal_statistics function."""
@pytest.fixture
def sample_deals(self):
"""Create sample deals for testing."""
return [
Deal(
objectid=1,
deal_amount=1000000.0,
deal_date="2024-01-15",
asset_area=80.0,
property_type_description="דירה",
),
Deal(
objectid=2,
deal_amount=1500000.0,
deal_date="2024-02-01",
asset_area=100.0,
property_type_description="דירת גג",
),
Deal(
objectid=3,
deal_amount=2000000.0,
deal_date="2024-03-01",
asset_area=150.0,
property_type_description="בית פרטי",
),
Deal(
objectid=4,
deal_amount=800000.0,
deal_date="2024-01-01",
asset_area=60.0,
property_type_description="דירה",
),
Deal(
objectid=5,
deal_amount=1200000.0,
deal_date="2024-04-01",
asset_area=120.0,
property_type_description="פנטהאוז",
),
]
def test_basic_statistics_calculation(self, sample_deals):
"""Test basic statistics calculation."""
stats = calculate_deal_statistics(sample_deals)
assert isinstance(stats, DealStatistics)
assert stats.total_deals == 5
assert "mean" in stats.price_statistics
assert "median" in stats.price_statistics
assert len(stats.property_type_distribution) > 0
def test_price_statistics(self, sample_deals):
"""Test price statistics calculation."""
stats = calculate_deal_statistics(sample_deals)
# Mean: (1000000 + 1500000 + 2000000 + 800000 + 1200000) / 5 = 1300000
assert stats.price_statistics["mean"] == 1300000.0
# Median of [800000, 1000000, 1200000, 1500000, 2000000] = 1200000
assert stats.price_statistics["median"] == 1200000.0
assert stats.price_statistics["min"] == 800000.0
assert stats.price_statistics["max"] == 2000000.0
assert "std_dev" in stats.price_statistics
assert "total" in stats.price_statistics
assert stats.price_statistics["total"] == 6500000.0
def test_area_statistics(self, sample_deals):
"""Test area statistics calculation."""
stats = calculate_deal_statistics(sample_deals)
# Mean: (80 + 100 + 150 + 60 + 120) / 5 = 102
assert stats.area_statistics["mean"] == 102.0
# Median of [60, 80, 100, 120, 150] = 100
assert stats.area_statistics["median"] == 100.0
assert stats.area_statistics["min"] == 60.0
assert stats.area_statistics["max"] == 150.0
def test_price_per_sqm_statistics(self, sample_deals):
"""Test price per square meter statistics."""
stats = calculate_deal_statistics(sample_deals)
# All deals have both price and area, so should have price_per_sqm
assert "mean" in stats.price_per_sqm_statistics
assert "median" in stats.price_per_sqm_statistics
assert stats.price_per_sqm_statistics["mean"] > 0
def test_property_type_distribution(self, sample_deals):
"""Test property type distribution."""
stats = calculate_deal_statistics(sample_deals)
dist = stats.property_type_distribution
assert dist["דירה"] == 2 # objectid 1 and 4
assert dist["דירת גג"] == 1
assert dist["בית פרטי"] == 1
assert dist["פנטהאוז"] == 1
def test_date_range(self, sample_deals):
"""Test date range calculation."""
stats = calculate_deal_statistics(sample_deals)
assert stats.date_range is not None
assert stats.date_range["earliest"] == "2024-01-01"
assert stats.date_range["latest"] == "2024-04-01"
def test_empty_deals_list(self):
"""Test statistics with empty deals list."""
stats = calculate_deal_statistics([])
assert stats.total_deals == 0
assert stats.price_statistics == {}
assert stats.area_statistics == {}
assert stats.price_per_sqm_statistics == {}
assert stats.property_type_distribution == {}
assert stats.date_range is None
def test_deals_with_missing_prices(self):
"""Test statistics when some deals have zero prices."""
deals = [
Deal(objectid=1, deal_amount=1000000.0, deal_date="2024-01-01", asset_area=80.0),
Deal(objectid=2, deal_amount=0.0, deal_date="2024-01-02", asset_area=100.0), # Zero price excluded
Deal(objectid=3, deal_amount=-1.0, deal_date="2024-01-03", asset_area=120.0), # Negative excluded
]
stats = calculate_deal_statistics(deals)
assert stats.total_deals == 3
# Only deals with positive prices are included
assert stats.price_statistics["mean"] == 1000000.0
assert len(stats.price_statistics) > 0 # Should still calculate stats for non-null values
def test_deals_with_missing_areas(self):
"""Test statistics when some deals have missing areas."""
deals = [
Deal(objectid=1, deal_amount=1000000.0, deal_date="2024-01-01", asset_area=80.0),
Deal(objectid=2, deal_amount=1500000.0, deal_date="2024-01-02", asset_area=None),
Deal(objectid=3, deal_amount=2000000.0, deal_date="2024-01-03"), # No asset_area
]
stats = calculate_deal_statistics(deals)
assert stats.total_deals == 3
assert stats.area_statistics["mean"] == 80.0 # Only one deal with area
assert len(stats.price_statistics) == 8 # All 3 deals have prices
def test_deals_with_missing_property_types(self):
"""Test statistics when some deals have missing property types."""
deals = [
Deal(objectid=1, deal_amount=1000000.0, deal_date="2024-01-01", property_type_description="דירה"),
Deal(objectid=2, deal_amount=1500000.0, deal_date="2024-01-02", property_type_description=None),
Deal(objectid=3, deal_amount=2000000.0, deal_date="2024-01-03"), # No property_type
]
stats = calculate_deal_statistics(deals)
assert stats.total_deals == 3
assert stats.property_type_distribution == {"דירה": 1}
def test_deals_with_zero_prices(self):
"""Test that zero prices are excluded from statistics."""
deals = [
Deal(objectid=1, deal_amount=1000000.0, deal_date="2024-01-01"),
Deal(objectid=2, deal_amount=0.0, deal_date="2024-01-02"), # Zero price
Deal(objectid=3, deal_amount=2000000.0, deal_date="2024-01-03"),
]
stats = calculate_deal_statistics(deals)
# Only 2 deals should be counted (excluding zero)
assert stats.price_statistics["mean"] == 1500000.0
def test_deals_with_zero_areas(self):
"""Test that zero areas are excluded from statistics."""
deals = [
Deal(objectid=1, deal_amount=1000000.0, deal_date="2024-01-01", asset_area=80.0),
Deal(objectid=2, deal_amount=1500000.0, deal_date="2024-01-02", asset_area=0.0),
Deal(objectid=3, deal_amount=2000000.0, deal_date="2024-01-03", asset_area=100.0),
]
stats = calculate_deal_statistics(deals)
assert stats.area_statistics["mean"] == 90.0 # (80 + 100) / 2
def test_single_deal(self):
"""Test statistics with single deal."""
deals = [
Deal(objectid=1, deal_amount=1000000.0, deal_date="2024-01-01", asset_area=80.0, property_type_description="דירה")
]
stats = calculate_deal_statistics(deals)
assert stats.total_deals == 1
assert stats.price_statistics["mean"] == 1000000.0
assert stats.price_statistics["median"] == 1000000.0
assert stats.price_statistics["min"] == 1000000.0
assert stats.price_statistics["max"] == 1000000.0
assert stats.price_statistics["std_dev"] == 0 # Only one value
def test_percentiles_calculation(self, sample_deals):
"""Test percentile calculations (p25, p75)."""
stats = calculate_deal_statistics(sample_deals)
# Sorted prices: [800000, 1000000, 1200000, 1500000, 2000000]
assert "p25" in stats.price_statistics
assert "p75" in stats.price_statistics
# p25 should be around 1000000, p75 around 1500000
assert stats.price_statistics["p25"] >= 800000
assert stats.price_statistics["p75"] <= 2000000
def test_invalid_input_raises_error(self):
"""Test that invalid input raises ValueError."""
with pytest.raises(ValueError, match="deals must be a list"):
calculate_deal_statistics("not a list")
def test_date_handling_with_date_objects(self):
"""Test date range calculation with date objects."""
deals = [
Deal(objectid=1, deal_amount=1000000.0, deal_date=date(2024, 1, 15), asset_area=80.0),
Deal(objectid=2, deal_amount=1500000.0, deal_date=date(2024, 2, 1), asset_area=100.0),
]
stats = calculate_deal_statistics(deals)
assert stats.date_range is not None
assert stats.date_range["earliest"] == "2024-01-15"
assert stats.date_range["latest"] == "2024-02-01"
def test_date_handling_with_iso_strings(self):
"""Test date range calculation with ISO format strings."""
deals = [
Deal(objectid=1, deal_amount=1000000.0, deal_date="2024-01-15T00:00:00.000Z", asset_area=80.0),
Deal(objectid=2, deal_amount=1500000.0, deal_date="2024-02-01T12:30:45.123Z", asset_area=100.0),
]
stats = calculate_deal_statistics(deals)
assert stats.date_range is not None
assert stats.date_range["earliest"] == "2024-01-15"
assert stats.date_range["latest"] == "2024-02-01"
def test_multiple_same_property_types(self):
"""Test property type distribution with duplicates."""
deals = [
Deal(objectid=1, deal_amount=1000000.0, deal_date="2024-01-01", property_type_description="דירה"),
Deal(objectid=2, deal_amount=1500000.0, deal_date="2024-01-02", property_type_description="דירה"),
Deal(objectid=3, deal_amount=2000000.0, deal_date="2024-01-03", property_type_description="דירה"),
]
stats = calculate_deal_statistics(deals)
assert stats.property_type_distribution["דירה"] == 3
def test_std_dev_calculation(self):
"""Test standard deviation calculation."""
deals = [
Deal(objectid=1, deal_amount=1000.0, deal_date="2024-01-01"),
Deal(objectid=2, deal_amount=2000.0, deal_date="2024-01-02"),
Deal(objectid=3, deal_amount=3000.0, deal_date="2024-01-03"),
]
stats = calculate_deal_statistics(deals)
assert "std_dev" in stats.price_statistics
assert stats.price_statistics["std_dev"] > 0
@pytest.mark.parametrize(
"deal_count,has_price,has_area,expected_price_stats,expected_area_stats",
[
(0, False, False, False, False), # Empty
(1, True, True, True, True), # Single deal
(3, True, False, True, False), # Deals with price only
(3, False, True, False, True), # Deals with area only (use zero for no price)
],
)
def test_statistics_with_various_data_availability(
self, deal_count, has_price, has_area, expected_price_stats, expected_area_stats
):
"""Parametrized test for statistics with varying data availability."""
deals = []
for i in range(deal_count):
deals.append(
Deal(
objectid=i,
deal_amount=1000000.0 if has_price else 0.0, # Use 0.0 instead of None
deal_date=f"2024-01-{i+1:02d}",
asset_area=80.0 if has_area else None,
)
)
stats = calculate_deal_statistics(deals)
assert stats.total_deals == deal_count
assert (len(stats.price_statistics) > 0) == expected_price_stats
assert (len(stats.area_statistics) > 0) == expected_area_stats
class TestCalculateStdDev:
"""Test cases for calculate_std_dev function."""
def test_std_dev_basic(self):
"""Test standard deviation calculation with basic values."""
values = [1.0, 2.0, 3.0, 4.0, 5.0]
std_dev = calculate_std_dev(values)
# Std dev of [1,2,3,4,5] with n-1 denominator ≈ 1.58
assert std_dev > 1.5
assert std_dev < 1.6
def test_std_dev_single_value(self):
"""Test std dev with single value returns 0."""
assert calculate_std_dev([5.0]) == 0.0
def test_std_dev_empty_list(self):
"""Test std dev with empty list returns 0."""
assert calculate_std_dev([]) == 0.0
def test_std_dev_identical_values(self):
"""Test std dev with identical values returns 0."""
assert calculate_std_dev([5.0, 5.0, 5.0, 5.0]) == 0.0
def test_std_dev_two_values(self):
"""Test std dev with two values."""
values = [10.0, 20.0]
std_dev = calculate_std_dev(values)
# Std dev of [10, 20] with n-1 denominator = sqrt((10^2 + 10^2)/1) ≈ 7.07
assert 7.0 < std_dev < 7.1
def test_std_dev_large_spread(self):
"""Test std dev with large value spread."""
values = [1.0, 100.0, 1000.0]
std_dev = calculate_std_dev(values)
# Large spread should have high std dev
assert std_dev > 500
@pytest.mark.parametrize(
"values,expected_range",
[
([1.0, 2.0, 3.0], (0.8, 1.2)), # Small range
([10.0, 20.0, 30.0], (8.0, 12.0)), # Medium range
([100.0, 200.0, 300.0], (80.0, 120.0)), # Large range
],
)
def test_std_dev_parametrized(self, values, expected_range):
"""Parametrized test for std dev with various value ranges."""
std_dev = calculate_std_dev(values)
assert expected_range[0] <= std_dev <= expected_range[1]