Skip to content

Commit 52e61fa

Browse files
Merge pull request #6 from reloc8/minor/bounding-box-and-score
Add bounding box and score for local statistics
2 parents e646400 + d8435df commit 52e61fa

8 files changed

Lines changed: 340 additions & 16 deletions

File tree

fetch_properties/core/mapper/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Any, AnyStr, Dict, List
22

33
from ..schema import Property, PropertyLocation, PropertiesPage, LocalStatistics, GlobalStatistics, Statistics, \
4-
PriceStatistics
4+
PriceStatistics, LocationBoundingBox
55

66

77
class PropertyMapper:
@@ -53,11 +53,13 @@ def map(local_statistics: List[LocalStatistics], global_statistics: GlobalStatis
5353
class LocalStatisticsMapper:
5454

5555
@staticmethod
56-
def map(price_statistics: PriceStatistics, geohash: AnyStr) -> LocalStatistics:
56+
def map(price_statistics: PriceStatistics, geohash: AnyStr, bounding_box: LocationBoundingBox, score) -> LocalStatistics:
5757

5858
mapped = LocalStatistics()
5959
mapped.price = price_statistics
6060
mapped.geohash = geohash
61+
mapped.bounding_box = bounding_box
62+
mapped.score = score
6163

6264
return mapped
6365

fetch_properties/core/schema/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ class LocalStatistics(graphene.ObjectType):
4444

4545
geohash = graphene.String()
4646
price = graphene.Field(PriceStatistics)
47+
bounding_box = graphene.Field(LocationBoundingBox)
48+
score = graphene.Int()
4749

4850

4951
class GlobalStatistics(graphene.ObjectType):

fetch_properties/core/schema/resolver.py

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import bson
22
import graphene
33
import os
4+
import pygeohash
45
import pymongo
56
from abc import ABC, abstractmethod
67
from dataclasses import dataclass
78

8-
from . import SearchBoundingBox, PropertyFilter, PropertiesPage, Statistics
9+
from . import SearchBoundingBox, PropertyFilter, PropertiesPage, Statistics, LocationBoundingBox, Point
910
from ..mapper import PropertyMapper, PropertiesPageMapper, LocalStatisticsMapper, PriceStatisticsMapper, \
1011
GlobalStatisticsMapper, StatisticsMapper
1112
from ..mongodb import MongoDBConnection, MONGODB_CONNECTION
@@ -189,19 +190,6 @@ def find_statistics_by_filter(self, filter: PropertyFilter) -> Statistics:
189190
}
190191
])
191192

192-
local_statistics = []
193-
for result in local_results:
194-
price_statistics = PriceStatisticsMapper.map(
195-
min_price=result.get('price').get('min'),
196-
max_price=result.get('price').get('max'),
197-
avg_price=result.get('price').get('avg')
198-
)
199-
geohash = result.get('geohash')
200-
local_statistics.append(LocalStatisticsMapper.map(
201-
price_statistics=price_statistics,
202-
geohash=geohash
203-
))
204-
205193
global_results = list(global_results)
206194

207195
min_price = None
@@ -220,10 +208,62 @@ def find_statistics_by_filter(self, filter: PropertyFilter) -> Statistics:
220208
)
221209
global_statistics = GlobalStatisticsMapper.map(price_statistics=global_price_statistics)
222210

211+
local_statistics = []
212+
for result in local_results:
213+
price_statistics = PriceStatisticsMapper.map(
214+
min_price=result.get('price').get('min'),
215+
max_price=result.get('price').get('max'),
216+
avg_price=result.get('price').get('avg')
217+
)
218+
geohash = result.get('geohash')
219+
local_statistics.append(LocalStatisticsMapper.map(
220+
price_statistics=price_statistics,
221+
geohash=geohash,
222+
bounding_box=self.get_bounding_box(geohash),
223+
score=self.get_score(result.get('price').get('avg'), avg_price)
224+
))
225+
223226
result = StatisticsMapper.map(local_statistics=local_statistics, global_statistics=global_statistics)
224227

225228
return result
226229

230+
@staticmethod
231+
def get_bounding_box(geohash) -> LocationBoundingBox:
232+
233+
bounding_box = LocationBoundingBox()
234+
bounding_box.top_right = Point()
235+
bounding_box.bottom_left = Point()
236+
237+
if geohash is not None:
238+
239+
decoded = pygeohash.decode_exactly(geohash)
240+
center_latitude = decoded[0]
241+
center_longitude = decoded[1]
242+
epsilon_latitude = decoded[2]
243+
epsilon_longitude = decoded[3]
244+
245+
bounding_box.top_right.latitude = center_latitude + epsilon_latitude
246+
bounding_box.top_right.longitude = center_longitude + epsilon_longitude
247+
248+
bounding_box.bottom_left.latitude = center_latitude - epsilon_latitude
249+
bounding_box.bottom_left.longitude = center_longitude - epsilon_longitude
250+
251+
return bounding_box
252+
253+
@staticmethod
254+
def get_score(local_avg, global_avg):
255+
256+
score = 0
257+
if local_avg is not None and global_avg is not None:
258+
if global_avg * 0.8 <= local_avg <= global_avg * 1.2:
259+
score = 50
260+
elif local_avg > global_avg:
261+
score = 0
262+
else:
263+
score = 100
264+
265+
return score
266+
227267

228268
RESOLVER_MONGODB = MongoDBResolver(
229269
mongodb_client=MONGODB_CLIENT,

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def private_dependency(personal_access_token: AnyStr,
3636
'dnspython',
3737
'pymongo',
3838
'graphene',
39+
'pygeohash',
3940
private_dependency(personal_access_token=GITHUB_PERSONAL_ACCESS_TOKEN,
4041
repo_user='reloc8', repo_name='lib-lambda-handler',
4142
package_name='lambda_handler', package_version='1.0.0')

tests/resources/collection-4.json

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
[
2+
{
3+
"_id": "d769abe7ca1d27e4129d5fd5ce137324df12dec2",
4+
"price": 135000,
5+
"monthly_charge": 55,
6+
"floor": "3",
7+
"n_rooms": 2,
8+
"n_bathrooms": 1,
9+
"n_parking_spaces": null,
10+
"surface": 52,
11+
"condition": "BEST",
12+
"type": "FLAT",
13+
"heating": "INDEPENDENT",
14+
"ownership": "FULL",
15+
"year_of_construction": 2012,
16+
"published_on": "2021-01-13",
17+
"location": {
18+
"point": {
19+
"type": "Point",
20+
"coordinates": [
21+
7.6634925,
22+
45.1038648
23+
]
24+
},
25+
"geohash": "u0j2w6umh"
26+
},
27+
"contract": "SALE",
28+
"cursor": "5ffc13f406610351150ae45a"
29+
},
30+
{
31+
"_id": "4da033af2bc14f7c477c37a1c417b0d335a4f018",
32+
"price": 350000,
33+
"monthly_charge": null,
34+
"floor": "1",
35+
"n_rooms": 5,
36+
"n_bathrooms": 2,
37+
"n_parking_spaces": 1,
38+
"surface": 180,
39+
"condition": "BEST",
40+
"type": "VILLA",
41+
"heating": "INDEPENDENT",
42+
"ownership": "FULL",
43+
"year_of_construction": null,
44+
"published_on": "2021-01-11",
45+
"location": {
46+
"point": {
47+
"type": "Point",
48+
"coordinates": [
49+
7.34602,
50+
45.0519856
51+
]
52+
},
53+
"geohash": "u0j0r1mny"
54+
},
55+
"contract": "SALE",
56+
"cursor": "5ffc13f406610351150ae45b"
57+
},
58+
{
59+
"_id": "2759a18c15f06900b2d2f561718be0bc8fdbc9ae",
60+
"price": 279000,
61+
"monthly_charge": null,
62+
"floor": null,
63+
"n_rooms": 5,
64+
"n_bathrooms": 2,
65+
"n_parking_spaces": 2,
66+
"surface": 160,
67+
"condition": "BEST",
68+
"type": "VILLA",
69+
"heating": "INDEPENDENT",
70+
"ownership": "FULL",
71+
"year_of_construction": null,
72+
"published_on": "2021-01-11",
73+
"location": {
74+
"point": {
75+
"type": "Point",
76+
"coordinates": [
77+
7.7302986,
78+
45.2913656
79+
]
80+
},
81+
"geohash": "u0j3xvj13"
82+
},
83+
"contract": "SALE",
84+
"cursor": "5ff9e97f114ae8feccbbe5bc"
85+
},
86+
{
87+
"_id": "f89ca2e9c0af9fad216dc62ca3ba5f64fb8040e9",
88+
"price": 149000,
89+
"monthly_charge": 100,
90+
"floor": "1",
91+
"n_rooms": 1,
92+
"n_bathrooms": 1,
93+
"n_parking_spaces": null,
94+
"surface": 45,
95+
"condition": "BEST",
96+
"type": "FLAT",
97+
"heating": "CENTRAL",
98+
"ownership": "FULL",
99+
"year_of_construction": 1985,
100+
"published_on": "2021-01-11",
101+
"location": {
102+
"point": {
103+
"type": "Point",
104+
"coordinates": [
105+
7.8782353,
106+
45.0343452
107+
]
108+
},
109+
"geohash": "u0j85q2b0"
110+
},
111+
"contract": "SALE",
112+
"cursor": "5ff9e97f114ae8feccbbe5bd"
113+
}
114+
]

tests/resources/query-4.graphql

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
statisticsByFilter(
3+
filter: {
4+
nRooms: {
5+
min: 2,
6+
max: 5
7+
},
8+
surface: {
9+
min: 40,
10+
max: 200
11+
},
12+
condition: "BEST"
13+
}
14+
) {
15+
localStatistics {
16+
geohash
17+
price {
18+
min
19+
max
20+
avg
21+
}
22+
boundingBox {
23+
topRight {
24+
latitude
25+
longitude
26+
}
27+
bottomLeft {
28+
latitude
29+
longitude
30+
}
31+
}
32+
score
33+
}
34+
globalStatistics {
35+
price {
36+
min
37+
max
38+
avg
39+
}
40+
}
41+
}
42+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"data": {
3+
"statisticsByFilter": {
4+
"localStatistics": [
5+
{
6+
"geohash": "u0j0r1m",
7+
"price": {
8+
"min": 350000,
9+
"max": 350000,
10+
"avg": 350000.0
11+
},
12+
"boundingBox": {
13+
"topRight": {
14+
"latitude": 45.05218505859375,
15+
"longitude": 7.34710693359375
16+
},
17+
"bottomLeft": {
18+
"latitude": 45.050811767578125,
19+
"longitude": 7.345733642578125
20+
}
21+
},
22+
"score": 0
23+
},
24+
{
25+
"geohash": "u0j3xvj",
26+
"price": {
27+
"min": 279000,
28+
"max": 279000,
29+
"avg": 279000.0
30+
},
31+
"boundingBox": {
32+
"topRight": {
33+
"latitude": 45.292510986328125,
34+
"longitude": 7.73162841796875
35+
},
36+
"bottomLeft": {
37+
"latitude": 45.2911376953125,
38+
"longitude": 7.730255126953125
39+
}
40+
},
41+
"score": 50
42+
},
43+
{
44+
"geohash": "u0j2w6u",
45+
"price": {
46+
"min": 135000,
47+
"max": 135000,
48+
"avg": 135000.0
49+
},
50+
"boundingBox": {
51+
"topRight": {
52+
"latitude": 45.1043701171875,
53+
"longitude": 7.664337158203125
54+
},
55+
"bottomLeft": {
56+
"latitude": 45.102996826171875,
57+
"longitude": 7.6629638671875
58+
}
59+
},
60+
"score": 100
61+
}
62+
],
63+
"globalStatistics": {
64+
"price": {
65+
"min": 135000,
66+
"max": 350000,
67+
"avg": 254666.66666666666
68+
}
69+
}
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)