AI-система подбора недвижимости
Поиск недвижимости — многомерная задача с высокой ценой ошибки. Покупатель не всегда точно формулирует критерии: «уютная квартира рядом с метро» нужно перевести в конкретные параметры. ML-система понимает неявные предпочтения из истории просмотров и уточняет профиль через диалог.
Модель предпочтений из поведения
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.metrics.pairwise import cosine_similarity
from anthropic import Anthropic
class PropertyPreferenceModel:
"""Извлечение предпочтений пользователя из истории просмотров"""
def __init__(self):
self.scaler = StandardScaler()
self.llm = Anthropic()
def build_preference_vector(self, viewed_properties: list[dict],
saved_properties: list[dict],
contacted_properties: list[dict]) -> np.ndarray:
"""
Взвешенный профиль из разных типов взаимодействий.
Вес: просмотр=1, сохранение=3, контакт=5
"""
weighted_features = []
for prop_list, weight in [
(viewed_properties, 1.0),
(saved_properties, 3.0),
(contacted_properties, 5.0)
]:
for prop in prop_list:
features = self._extract_features(prop)
weighted_features.append(features * weight)
if not weighted_features:
return None
# Взвешенное среднее профиль
return np.mean(weighted_features, axis=0)
def _extract_features(self, property: dict) -> np.ndarray:
"""Числовой вектор объекта недвижимости"""
return np.array([
property.get('price_m2', 0) / 200000, # Нормализованная цена/м²
property.get('area_m2', 0) / 150, # Площадь
property.get('rooms', 0) / 5, # Комнат
property.get('floor', 0) / 25, # Этаж
property.get('floor_total', 0) / 25, # Этажность дома
property.get('metro_minutes', 99) / 60, # Минут до метро
int(property.get('new_building', False)), # Новостройка
int(property.get('has_parking', False)), # Парковка
int(property.get('balcony', False)), # Балкон
property.get('ceiling_height', 2.5) / 4.0, # Высота потолков
int(property.get('renovation', 'none') == 'euro'), # Евроремонт
int(property.get('renovation', 'none') == 'designer'),
property.get('year_built', 1990) / 2024, # Год постройки
])
def find_similar_properties(self, user_preference: np.ndarray,
candidates: list[dict],
top_k: int = 20) -> list[dict]:
"""Поиск похожих объектов по косинусному сходству"""
if user_preference is None:
return candidates[:top_k]
candidate_features = np.array([
self._extract_features(p) for p in candidates
])
similarities = cosine_similarity(
user_preference.reshape(1, -1), candidate_features
)[0]
for i, prop in enumerate(candidates):
prop['match_score'] = float(similarities[i])
return sorted(candidates, key=lambda x: x['match_score'], reverse=True)[:top_k]
class PropertySearchAssistant:
"""Диалоговый агент для уточнения параметров поиска"""
def __init__(self):
self.llm = Anthropic()
self.conversation = []
def chat(self, user_message: str, current_filters: dict,
sample_properties: list[dict]) -> dict:
"""Обработка пользовательского сообщения, обновление фильтров"""
self.conversation.append({"role": "user", "content": user_message})
import json
response = self.llm.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=400,
system="""You are a real estate search assistant. Help users find properties.
Extract search filters from conversation. Respond in Russian.
Current filters (JSON): """ + json.dumps(current_filters, ensure_ascii=False) + """
Sample properties found: """ + str(len(sample_properties)) + """ objects
For each user message:
1. Update search filters based on what they said
2. Ask 1 clarifying question if important parameters are missing
3. Summarize what you understood
Return JSON: {"filters": {...}, "clarifying_question": "...", "summary": "..."}""",
messages=self.conversation
)
assistant_text = response.content[0].text
self.conversation.append({"role": "assistant", "content": assistant_text})
try:
parsed = json.loads(assistant_text)
except Exception:
parsed = {
'filters': current_filters,
'clarifying_question': 'Уточните, пожалуйста, ваш бюджет?',
'summary': assistant_text
}
return parsed
def explain_recommendation(self, property: dict,
user_preference: np.ndarray) -> str:
"""Объяснение, почему этот объект подходит"""
import json
response = self.llm.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=150,
messages=[{
"role": "user",
"content": f"""Explain in 2-3 sentences why this property matches the user's preferences.
Property: {json.dumps(property, ensure_ascii=False)}
Match score: {property.get('match_score', 0):.0%}
Speak Russian, be specific about the best features."""
}]
)
return response.content[0].text
class NeighborhoodScorer:
"""Скоринг района по POI (Points of Interest)"""
def score_location(self, lat: float, lon: float,
user_priorities: dict) -> dict:
"""
user_priorities: {'schools': 3, 'metro': 5, 'parks': 2, 'shopping': 1}
POI данные получаются из OpenStreetMap / Яндекс Геосервисов
"""
# Заглушка для реальных API-вызовов
poi_distances = {
'metro': 8, # минут пешком
'schools': 12,
'parks': 5,
'shopping': 15,
'hospitals': 20,
}
score = 0
max_score = sum(user_priorities.values())
for poi_type, priority in user_priorities.items():
distance = poi_distances.get(poi_type, 30)
# Оценка: 10 мин = хорошо, 20+ мин = плохо
poi_score = max(0, 1 - (distance - 5) / 20)
score += poi_score * priority
return {
'total_score': round(score / max_score, 2),
'poi_breakdown': {k: v for k, v in poi_distances.items()},
'walkability': self._compute_walkability(poi_distances)
}
def _compute_walkability(self, poi_distances: dict) -> str:
avg_distance = np.mean(list(poi_distances.values()))
if avg_distance < 10:
return 'excellent'
elif avg_distance < 15:
return 'good'
elif avg_distance < 20:
return 'average'
return 'poor'
Интеграция с ценовой аналитикой
Система автоматически помечает объекты как «выгодные» или «переоценённые» на основе регрессионной модели справедливой цены:
class PropertyPriceEstimator:
def assess_value(self, property: dict, market_data: pd.DataFrame) -> dict:
"""Оценка рыночной справедливости цены"""
# GBT модель обучена на транзакциях последних 6 месяцев
similar = market_data[
(market_data['district'] == property.get('district')) &
(market_data['rooms'] == property.get('rooms')) &
(abs(market_data['area_m2'] - property.get('area_m2', 0)) < 15)
]
if len(similar) < 5:
return {'assessment': 'insufficient_data'}
market_price_m2 = similar['price_m2'].median()
property_price_m2 = property.get('price', 0) / max(property.get('area_m2', 1), 1)
premium_pct = (property_price_m2 - market_price_m2) / market_price_m2 * 100
if premium_pct < -10:
assessment = 'underpriced'
elif premium_pct > 15:
assessment = 'overpriced'
else:
assessment = 'fair_price'
return {
'assessment': assessment,
'market_price_m2': round(market_price_m2),
'property_price_m2': round(property_price_m2),
'premium_pct': round(premium_pct, 1),
'similar_count': len(similar)
}
Типичные результаты внедрения: время поиска объекта сокращается с 3-6 недель до 1-2, количество нерелевантных просмотров снижается на 40-60%. Конверсия в звонок брокеру растёт на 20-30% за счёт предварительной квалификации интереса через диалог.







