Условие
В файле hotels_parsing_result.xlsx две вкладки platform1 и platform2 — данные о гостиницах с двух платформ:
- Записи могут повторяться, последняя — самая актуальная.
- Если поле в последней записи пустое — взять из более ранней.
- Поля
phone,email,website— строки с несколькими значениями через запятую и лишними символами.
Задача: для каждой платформы вывести по одной записи на гостиницу (по uid) с самыми полными и актуальными данными; распарсить контакты в списки нормализованного формата; вывести топ-10 по числу телефонов и числу отзывов; найти квадрат 1 км × 1 км с максимальным числом гостиниц; объединить платформы и найти топ-10 общих по сумме отзывов.
Решение
1. Дедупликация с заполнением пропусков
import pandas as pd, re
def consolidate(df):
df = df.sort_values('parsing_time')
# Берём «последнее не-NaN» по каждому полю в группе uid
out = df.groupby('uid', as_index=False).agg(
lambda s: s.dropna().iloc[-1] if s.dropna().size else None
)
return out
p1 = consolidate(pd.read_excel('hotels.xlsx', sheet_name='platform1'))
p2 = consolidate(pd.read_excel('hotels.xlsx', sheet_name='platform2'))2. Парсинг контактов
PHONE_RE = re.compile(r'\+?\d[\d\s\-()]{6,}\d')
EMAIL_RE = re.compile(r'[\w\.\-]+@[\w\.\-]+\.\w+')
URL_RE = re.compile(r'(?:https?://)?[\w\-]+\.[\w\-\.]+')
def normalize_phone(s):
digits = re.sub(r'\D', '', s)
if len(digits) == 11 and digits.startswith('8'):
digits = '7' + digits[1:]
return digits
def parse_phones(cell):
if pd.isna(cell): return []
return [normalize_phone(x) for x in PHONE_RE.findall(str(cell))]
def parse_emails(cell):
if pd.isna(cell): return []
return [m.lower() for m in EMAIL_RE.findall(str(cell))]
p1['phones'] = p1.phone.map(parse_phones)
p1['emails'] = p1.email.map(parse_emails)3. Топ-10 и квадрат 1×1 км
# Топ-10 по числу телефонов
p1.assign(n_phones=p1.phones.str.len()).nlargest(10, 'n_phones')
# Топ-10 по review_count
p1.nlargest(10, 'review_count')
# 1 км × 1 км: округляем lat/lon в шаге ~0.009° (1 км по широте)
p1['lat_bin'] = (p1.lat / 0.009).round().astype(int)
p1['lon_bin'] = (p1.lon / (0.009 / np.cos(np.radians(p1.lat.mean())))).round().astype(int)
top_grid = p1.groupby(['lat_bin','lon_bin']).size().nlargest(1)4. Объединение платформ
Критерий слияния — нечёткое совпадение названия + расстояние < 100 м (или совпадение по URL/email). Для топ-10 общих — сумма review_count от обеих платформ.
from rapidfuzz import fuzz
merged = p1.merge(p2, on='lat_bin', suffixes=('_1','_2'))
merged['sim'] = merged.apply(lambda r: fuzz.token_set_ratio(r.title_1, r.title_2), axis=1)
matched = merged[merged.sim > 85]
matched['reviews_total'] = matched.review_count_1 + matched.review_count_2
matched.nlargest(10, 'reviews_total')Подводные камни
- «Последнее не пустое» работает корректно только при сортировке по
parsing_time. На равных временных метках — порядок недетерминирован. - Длина 1 км по долготе зависит от широты —
1 км ≈ 0.009° / cos(lat). - Объединение платформ: разные форматы названий (
'Hotel ABC'vs'ABC Hotel') — нужен fuzzy match. phoneможет содержатьwhatsapp 8800...— регулярка должна не цепляться за слова.
Эталонный ответ
groupby(uid).agg(last-non-null) для дедупа; регулярки для парсинга контактов; биннинг по lat/lon с поправкой на широту; fuzzy-merge для объединения платформ.