Exemplo n.º 1
0
class HouseScore(object):
    DEFAULTS = {
        "distance": {
            "median": 8.0,  # stay in King County
            "div": 0.5,
            "weight": -9000.0,  # one mile costs ( $900 / year ) * 10 years
            "cutoff": 18.0
        },
        "area": {
            "median": 2700.0,  # We are looking for a house around this area
            "div": 1.0,
            "weight": 225.0,  # a good house would cost $275 per SF
            "cutoff": 1900.0
        },
        "build": {
            "median": 2018.0,
            "div": 1.0,
            "weight": -2000.0,
            "cutoff": 1990.0
        },
        "beds": {
            "median": 4.0,
            "div": 1.0,
            "weight": 10000.0,  # for each extra room you get 10K
            "cutoff": 3.0
        },
        "backyard": {
            "median": 2500.0,  # not used
            "div": 1.0,
            "weight": 5.0,  # I'd pay extra 25K for 5000 SF back yard
            "cutoff": 500.0
        },
        "crime": {
            "median": 236.5,
            "div": 1.0,
            "weight": 1.0,
            "cutoff": -10.0
        },
        "history": {
            "median": 7.0,
            "div": 1000.0,  # lose 0.1% every day on market
            "weight": 1.0,  # penelty = dom * price / div
            "cutoff":
            155.0,  # if no human finds this house good for 5 months, don't consider it!
            "history_days": 365.0,
            "pending_penelty": 10000.0,
            "inspection_penelty": 25000.0,
            "delisted_penelty": 5000.0
        },
        "layout": {
            "median": 0.0,  # not used
            "div": 0.01,
            "weight": 1.0,
            "cutoff": 0.0,  # not used
            "beds_min": 3.0,
            "bed_bonus": 10000.0,
            "baths_min": 2.5,
            "bath_bonus": 2000.0,
            "required": {
                "Attached Garage": 5000.0,
                "Living Room": 5000.0,
                "Dining Room": 5000.0
            },
            "optional": {
                "Bonus Room": 1000.0,
                "Family Room": 5000.0,
                "Recreation Room": 1000.0,
                "Walk-In Closet": 1000.0,
                "Utility Room": 1000.0,
                "Loft": 1000.0,
                "Den": 3000.0,
                "Office": 1000.0
            }
        },
        "amenities": {
            "median": 95.0,  # not used
            "div": 0.01,
            "weight": 1.0,
            "cutoff": 80.0,  # should have 80% of what we are looking for
            "required": {
                "Forced Air Heating": 10000.0,
                "Dishwasher": 2000.0,
                "Dryer": 1000.0,
                "Oven": 1000.0,
                "Refrigerator": 3000.0,
                "'Washer'": 1000.0,
                "Public Water Source": 5000.0,
                "Sewer Connected": 5000.0,
                "Garbage Disposal": 1000.0,
                "High Speed Internet": 5000.0
            },
            "optional": {
                "Microwave": 500.0,
                "Composition Roof": 2000.0,
                "Central Air Conditioning": 10000.0,
                "'King County'": 100000.0,
                "'Bothell'": 50000.0,
                "'Kenmore'": 50000.0,
                "'Brier'": 20000.0
            }
        },
        "value": {
            # always run this last
            "total_cutoff": 500000.0,
            "percentage_cutoff": -50.0
        }
    }

    def __init__(self,
                 default_fields=None,
                 fav_locations=None,
                 cahe_folder=CACHE_DIR):
        super(HouseScore, self).__init__()

        self.cahe_folder = cahe_folder
        self.rfapi = RFAPI(cahe_folder=cahe_folder)

        if default_fields is not None:
            self.default_fields = default_fields
        else:
            self.default_fields = HouseScore.DEFAULTS.copy()

        if fav_locations is not None:
            self.fav_locations = fav_locations
        else:
            self.fav_locations = HouseScore.LoadFavorits(
                os.path.join(SCRIPT_DIR, "FavoriteLocations.json"))

    def value(self, house, details=None, total_score=0.0):
        total_cutoff = self.default_fields["value"]["total_cutoff"]
        percentage_cutoff = self.default_fields["value"]["percentage_cutoff"]

        details_available = details is not None

        message = []
        accepted = True

        if total_score < total_cutoff and details_available:
            accepted = False
            message.append("house valued at {} which is less than {}".format(
                total_score, total_cutoff))

        gain = total_score - float(house["price"])
        gain_percentage = gain / total_score * 100
        if gain_percentage < percentage_cutoff and details_available:
            accepted = False
            message.append("house ROI of {} which is less than {}".format(
                gain_percentage, percentage_cutoff))

        return HouseScoreResult(total_score, gain_percentage, accepted,
                                ", ".join(message))

    def get_scores(self, house, details=None):
        scores = {}
        total_score = 0.0
        for k in self.default_fields:
            if k == "value": continue
            method = getattr(self, k, None)
            if method is None: continue
            result = method(house, details)
            scores[k] = dict(**result._asdict())
            total_score += result.money
            if not result.accepted:
                scores["cutoff"] = ",".join(
                    filter(None, [scores.get("cutoff"), k]))

        # value evaluation must be run last
        result = self.value(house, details, total_score)
        scores["value"] = dict(**result._asdict())
        if not result.accepted:
            scores["cutoff"] = ",".join(
                filter(None, [scores.get("cutoff"), "value"]))

        scores["facts"] = {
            "build":
            house.get("year_built", -1),
            "full_address":
            RFAPI.house_address(house),
            "beds":
            house["beds"],
            "sqft":
            house["sqft"] if house.get("sqft") is not None else 0.0,
            "baths":
            house.get("baths", 0),
            "price":
            house["price"],
            "County":
            RFAPI.house_county(house, details),
            "photo":
            RFAPI.house_photo_url(house, details),
            "neighborhoods":
            RFAPI.house_neighborhoods(house, details)
            if details is not None else []
        }
        return scores

    def _get_measure_facts(self, measure_name):
        median = self.default_fields[measure_name]["median"]
        div = self.default_fields[measure_name]["div"]
        cutoff = self.default_fields[measure_name]["cutoff"]
        weight = self.default_fields[measure_name]["weight"]
        return (median, div, cutoff, weight)

    def area(self, house, details):
        median, div, cutoff, weight = self._get_measure_facts("area")
        if house.get("sqft") is None:
            return HouseScoreResult(0.0, 0.0, False, "missing 'sqft'")
        area = house["sqft"]
        money = area * weight
        message = "" if area >= cutoff else "area {}(sf) is less than cut of {}(sf)".format(
            area, cutoff)
        return HouseScoreResult(money, area, (area >= cutoff), message)

    def build(self, house, details):
        median, div, cutoff, weight = self._get_measure_facts("build")
        if house.get("year_built") is None:
            return HouseScoreResult(0.0, 0.0, False, "missing 'year_built'")
        year_built = house["year_built"]
        money = (median - year_built) / div * weight
        message = "" if year_built >= cutoff else "House is built in {}, older than cut of {}".format(
            year_built, cutoff)
        return HouseScoreResult(money, year_built, (year_built >= cutoff),
                                message)

    def backyard(self, house, details):
        if house.get("lotsize") is None or house.get("sqft") is None:
            return HouseScoreResult(0.0, 0.0, False,
                                    "missing 'lotsize' or 'sqft'")
        median, div, cutoff, weight = self._get_measure_facts("backyard")
        remaining_for_backyard = float(house["lotsize"]) - float(house["sqft"])
        money = remaining_for_backyard * weight
        message = "" if remaining_for_backyard >= cutoff else "backyard {}(sf) is less than cut of {}(sf)".format(
            remaining_for_backyard, cutoff)
        return HouseScoreResult(money, remaining_for_backyard,
                                (remaining_for_backyard >= cutoff), message)

    def amenities(self, house, details):
        return self.amenitiesInfo("amenities", house, details)

    def layout(self, house, details):
        info = self.amenitiesInfo("layout", house, details)
        money_score = info.money
        # check beds / baths
        if house.get("beds") is not None:
            extra_beds = house["beds"] - self.default_fields["layout"][
                "beds_min"]
            money_score += extra_beds * self.default_fields["layout"][
                "bed_bonus"]
        if house.get("baths") is not None:
            extra_baths = house["baths"] - self.default_fields["layout"][
                "baths_min"]
            money_score += extra_baths * self.default_fields["layout"][
                "bath_bonus"]

        return HouseScoreResult(money_score, info.value, info.accepted,
                                info.message)

    def dom(self, house, details):
        median, div, cutoff, weight = self._get_measure_facts("history")
        now_epoch = time.time() * 1000.0
        oldest_epoch = now_epoch - self.default_fields["history"][
            "history_days"] * MS_IN_A_DAY

        house["dom_fixed"] = house["dom"] if house.get(
            "dom") is not None else 0.0
        if details is not None:
            oldest_history_after_sale = now_epoch
            for event in details["payload"]["propertyHistoryInfo"]["events"]:
                if "sold" in event["eventDescription"].lower() or event[
                        "eventDate"] < oldest_epoch:  # or event["historyEventType"] != 1
                    break  # assuming events are sorted!
                event_epoc = event["eventDate"]
                if event_epoc < oldest_history_after_sale:
                    #logging.debug("#1 oldest = {}, this event = {}".format(oldest_history_after_sale,event_epoc))
                    oldest_history_after_sale = event_epoc
            oldest_history_after_sale_days = (
                now_epoch - oldest_history_after_sale) / MS_IN_A_DAY
            #logging.debug("dom={}, dom_fixed={}".format(house["dom"], oldest_history_after_sale_days))
            house["dom_fixed"] = max(house["dom"],
                                     oldest_history_after_sale_days)
        if house.get("dom") is None:
            return HouseScoreResult(0.0, 0.0, False, "missing 'dom'")
        dom = house["dom_fixed"]
        money = (median - dom) / div * float(house["price"])
        message = "House was on the market for {} days".format(int(dom))
        if int(dom) != int(house["dom"]):
            message = message + " (reported {} days)".format(int(house["dom"]))
        if dom > cutoff:
            message = message + ", more than cut of {}".format(int(cutoff))
        return HouseScoreResult(money, dom, (dom < cutoff), message)

    @staticmethod
    def AddIfNotExists(list_to_append, element):
        if element not in list_to_append:
            list_to_append.append(element)
        return list_to_append

    def history(self, house, details):
        na = HouseScoreResult(0, 0.0, True, "Not enough details")
        if details is None:
            return na
        if details.get("payload") is None or details.get("payload").get(
                "propertyHistoryInfo") is None:
            return na
        median, div, cutoff, weight = self._get_measure_facts("history")
        messages = []
        dom_results = self.dom(house, details)
        messages.append(dom_results.message)

        epoch_days = time.time() * 1000.0 / MS_IN_A_DAY
        oldest_epoch_days = epoch_days - self.default_fields["history"][
            "history_days"]
        oldest_epoch_days = max(house["dom_fixed"], oldest_epoch_days)
        total_penelty = 0.0
        for event in details["payload"]["propertyHistoryInfo"]["events"]:
            event_epoc = event["eventDate"]
            event_epoc_days = event_epoc / MS_IN_A_DAY
            event_epoc_diff = int(epoch_days - event_epoc_days)
            event_str = json.dumps(event).lower()

            #logging.debug("#2 oldest = {}, this event = {}, diff = {}".format(oldest_epoch_days,event_epoc_days,event_epoc_diff))
            if event_epoc_days < oldest_epoch_days:
                continue

            #logging.debug("event = {}".format(event_str))

            if "inspection" in event_str:
                total_penelty -= self.default_fields["history"][
                    "inspection_penelty"]
                HouseScore.AddIfNotExists(
                    messages, "was pending inspection {} days ago".format(
                        event_epoc_diff))
            elif "pending" in event_str:
                total_penelty -= self.default_fields["history"][
                    "pending_penelty"]
                HouseScore.AddIfNotExists(
                    messages,
                    "was pending {} days ago".format(event_epoc_diff))
            elif event["eventDescription"].lower() in ["delisted", "relisted"]:
                total_penelty -= self.default_fields["history"][
                    "delisted_penelty"]
                HouseScore.AddIfNotExists(
                    messages,
                    "was relisted {} days ago".format(event_epoc_diff))

        return HouseScoreResult(total_penelty + dom_results.money,
                                dom_results.value, dom_results.accepted,
                                ", ".join(messages))

    def amenitiesInfo(self, key, house, details):
        na = HouseScoreResult(0, 0.0, True, "Not enough details")
        if details is None:
            return na
        if details.get("payload") is None or details.get("payload").get(
                "amenitiesInfo") is None:
            return na
        median, div, cutoff, weight = self._get_measure_facts(key)
        #logging.debug("amenities : detials = {}".format(details))
        amenities_str = str(details["payload"]).lower()
        #logging.debug("amenities_str = {}".format(amenities_str)) #disable me
        required_amenities_sum = 0.0
        amenities_score = 0.0
        missing_amenities = []
        for k in self.default_fields[key]["required"]:
            v = self.default_fields[key]["required"][k]
            required_amenities_sum = required_amenities_sum + v
            if k.lower() in amenities_str:
                amenities_score = amenities_score + v
            else:
                missing_amenities.append(k)

        for k in self.default_fields[key]["optional"]:
            if k.lower() in amenities_str:
                amenities_score = amenities_score + self.default_fields[key][
                    "optional"][k]
            else:
                missing_amenities.append(k)

        missing_amenities_message = "" if len(
            missing_amenities) == 0 else "missing : [ {} ]".format(
                ",".join(missing_amenities))
        percentage = amenities_score / required_amenities_sum / div
        message = missing_amenities_message if percentage >= cutoff else " percentage {} is less than cut of {}, {}".format(
            percentage, cutoff, missing_amenities_message)

        return HouseScoreResult(amenities_score, percentage,
                                (percentage >= cutoff), message)

    def distance(self, house, details):
        ret = 0.0
        median, div, cutoff, weight = self._get_measure_facts("distance")
        fav_len = len(self.fav_locations)
        if fav_len == 0 or house.get("parcel") is None or house.get(
                "parcel").get("longitude") is None:
            house_address = RFAPI.house_address(house)
            if house_address != "":
                loc = AddressToLocation(house_address)
                if loc is not None and len(loc) == 2:
                    house["parcel"] = {"latitude": loc[0], "longitude": loc[1]}
            return HouseScoreResult(cutoff * weight, cutoff, False,
                                    "Can't measure distance")

        for fav in self.fav_locations:
            dist = LocationDistance(
                [house["parcel"]["latitude"], house["parcel"]["longitude"]],
                fav["loc"])
            diff = dist / div
            ret = ret + diff

        distance_average = ret / fav_len
        money = distance_average * weight
        message = "" if distance_average <= cutoff else "distance {}(mil) is larger than cut of {}(mil)".format(
            distance_average, cutoff)
        return HouseScoreResult(money, distance_average,
                                (distance_average < cutoff), message)

    @staticmethod
    def get_house_score_message(m):
        scores = m['scores']
        id_str = RFAPI.house_address(m)

        if m.get("URL") is not None:
            id_str = "{} : http://www.redfin.com{}".format(id_str, m["URL"])

        if scores.get("cutoff") is not None:
            return False, "{} => Cut for {{ {} }} score = {}".format(
                id_str, scores["cutoff"], scores)
        else:
            return True, ("{} => {}".format(id_str, scores))

    def post_process_one(self, m, search_name, get_details=False, force=False):
        house_details = None
        if get_details:
            house_details = self.rfapi.get_house_details(m,
                                                         force=force,
                                                         cache_time_format="")

        #logging.debug("post_process(get_details={})=>{}".format(get_details, house_details))
        RFAPI.generate_url_for_house(m)
        ha = RFAPI.house_address_parts(m)
        scores = None
        try:
            scores = self.get_scores(m, house_details)
        except:
            logging.error("house : {}, throw {}".format(
                json.dumps(m), traceback.format_exc()))
        if scores is None:
            return m

        m['scores'] = scores
        is_good, message = HouseScore.get_house_score_message(m)

        if is_good:
            if house_details is None:
                house_details = self.rfapi.get_house_details(
                    m, force=force, cache_time_format="")
                scores = self.get_scores(m, house_details)
            house_neighborhoods = RFAPI.house_neighborhoods(m, house_details)
            m['scores'] = scores
            is_good, message = HouseScore.get_house_score_message(m)

            logging.info(message)
            # getting city data takes a long time, will do it only for winning houses!
            """
            self.city_data.get_data(
                house_address= ha['display']
                , city=ha['city']     # Everett
                , state_short=ha['state']
                , zip_code=ha['zip']
                , house_neighborhoods=house_neighborhoods
                , force=force)
            """
        else:
            logging.debug(message)

        return m

    def post_process(self,
                     matches,
                     search_name,
                     get_details=False,
                     force=False):
        good_ones = 0

        with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor:
            executor.map(
                lambda m: self.post_process_one(m,
                                                search_name=search_name,
                                                get_details=get_details,
                                                force=force), matches)

        logging.debug("Done parallel processing")

        for m in tqdm(matches):
            #     m = self.post_process_one(m, search_name=search_name, get_details=get_details, force=force)

            is_good, message = HouseScore.get_house_score_message(m)
            if is_good:
                good_ones = good_ones + 1

        logging.debug("{} Matches={}/{}".format(search_name, good_ones,
                                                len(matches)))
        return matches

    @staticmethod
    def _plot_groups(X, y_pred):
        colors = np.array(
            list(
                islice(
                    cycle([
                        '#377eb8', '#ff7f00', '#4daf4a', '#f781bf', '#a65628',
                        '#984ea3', '#999999', '#e41a1c', '#dede00'
                    ]), int(max(y_pred) + 1))))
        plt.scatter(X[:, 0], X[:, 1], s=10, color=colors[y_pred])
        plt.show()

    @staticmethod
    def ClusterHouses(matches, plot_groups=False):
        groups = {}
        try:
            N = len(matches)
            X = np.zeros((N, 2))
            for m in range(N):
                loc = RFAPI.house_location(matches[m])
                #logging.debug("ClusterHouses({})".format(loc))
                X[m] = (loc[0], loc[1])

            params = {
                'quantile': .3,
                'eps': .15,
                'damping': .9,
                'preference': -5,
                'n_neighbors': 2,
                'n_clusters': 5
            }

            # a bit buggy..
            spectral = cluster.SpectralClustering(
                n_clusters=params['n_clusters'],
                eigen_solver='arpack',
                affinity="nearest_neighbors")

            # best so far!
            gmm = mixture.GaussianMixture(n_components=params['n_clusters'],
                                          covariance_type='full')

            # yielded one cluster..
            affinity_propagation = cluster.AffinityPropagation(
                damping=params['damping'], preference=params['preference'])

            bandwidth = cluster.estimate_bandwidth(X,
                                                   quantile=params['quantile'])
            ms = cluster.MeanShift(bandwidth=bandwidth, bin_seeding=True)

            algorithm = ms

            algorithm.fit(X)
            if hasattr(algorithm, 'labels_'):
                y_pred = algorithm.labels_.astype(np.int)
            else:
                y_pred = algorithm.predict(X)
            for m in range(len(matches)):
                key = str(y_pred[m])
                if groups.get(key, None) == None:
                    groups[key] = []

                groups[key].append({
                    "adress": RFAPI.house_address(matches[m]),
                    "location": [X[m][0], X[m][1]]
                })
            logging.debug("groups = {}".format(groups))
            if plot_groups:
                HouseScore._plot_groups(X, y_pred)
        except Exception as e:
            groups["error"] = str(e)
            logging.error(groups["error"])
        return groups

    @staticmethod
    def filter_good_houses(houses):
        return [m for m in houses if m['scores'].get("cutoff") is None]

    @staticmethod
    def add_html_tab(tab_id, tab_name, tab_content, tabs):
        is_first_tab = False
        tab_template = '<button class="tablinks {2}" onclick="openTab(event, \'{0}\')">{1}</button>'
        tab_content_template = '<div id="{0}" class="tabcontent">{1}</div>'

        if tabs is None:
            tabs = {"tabs": ['<div class="tab">', '</div>'], "contents": []}
            is_first_tab = True

        tabs['tabs'].insert(
            len(tabs['tabs']) - 1,
            tab_template.format(tab_id, tab_name,
                                'defaultOpen' if is_first_tab else ""))
        tabs['contents'].append(
            tab_content_template.format(tab_id, tab_content))
        return tabs

    @staticmethod
    def get_house_summary(house, rank=0):
        score = house.get("scores")
        if score is None:
            logging.warning("score is missing for {}".format(house))
            return {}
        first_listed = datetime.datetime.today() - datetime.timedelta(
            days=score["history"]["value"])
        house_summary = {
            "Address":
            score["facts"]["full_address"],
            "score":
            score["value"]["value"],
            "distance":
            score["distance"]["value"],
            "County":
            score["facts"]["County"],
            "Year Build":
            score["facts"]["build"],
            "beds":
            score["facts"]["beds"],
            "baths":
            score["facts"]["baths"],
            "price":
            score["facts"]["price"],
            "sqft":
            score["facts"]["sqft"],
            "lot size":
            score["facts"]["sqft"] + score["backyard"]["value"],
            "first_listed":
            first_listed.strftime("%m/%d/%Y"),
            "dom":
            score["history"]["message"],
            "url":
            "http://www.redfin.com{}".format(house["URL"]),
            "picture":
            score["facts"]["photo"],
            "price_for_sf":
            score["facts"]["price"] /
            score["facts"]["sqft"] if score["facts"]["sqft"] > 0 else 0.0,
            "user_rank":
            rank
        }
        return house_summary

    @staticmethod
    def get_houses_category_html(houses, summary="", category_id=""):
        html_content = []
        #logging.debug("houses {}".format(len(houses)))
        even_raw = False
        house_id = 0
        for house in houses:
            house_id += 1

            even_raw = not even_raw
            score = house.get("scores")
            if score is None:
                logging.warning("score is missing for {}".format(house))
                continue
            house_summary_raw = HouseScore.get_house_summary(house)
            house_summary = {
                "Address": house_summary_raw["Address"],
                "score": "{:.2f}".format(house_summary_raw["score"]),
                "distance": "{:.2f}".format(house_summary_raw["distance"]),
                "County": house_summary_raw["County"],
                "Year Build": house_summary_raw["Year Build"],
                "beds": house_summary_raw["beds"],
                "baths": house_summary_raw["baths"],
                "price": house_summary_raw["price"],
                "sqft": house_summary_raw["sqft"],
                "lot size": house_summary_raw["lot size"],
                "dom": house_summary_raw["dom"],
                "$/sf": "{:.0f}".format(house_summary_raw["price_for_sf"])
            }

            is_good, message = HouseScore.get_house_score_message(house)
            if is_good:
                pass_message = "Good ( score = {:.2f}, distance = {:.2f} )".format(
                    score["value"]["value"], score["distance"]["value"])
            else:
                house_summary["Failed"] = "[ {} ]".format(score["cutoff"])
                pass_message = "Failed [ {} ] ( score = {:.2f}, distance = {:.2f} )".format(
                    score["cutoff"], score["value"]["value"],
                    score["distance"]["value"])

            house_summary_html = DicToTHML(house_summary)
            score_html = DicToTHML(score)

            # build taps
            tabs = None
            house_group_id = "{}_{}".format(category_id, house_id)
            tabs = HouseScore.add_html_tab(
                tab_id="summary_{}".format(house_group_id),
                tab_name="Summary",
                tab_content=house_summary_html,
                tabs=tabs)
            tabs = HouseScore.add_html_tab(
                tab_id="details_{}".format(house_group_id),
                tab_name="Details",
                tab_content=score_html,
                tabs=tabs)

            maps_url = "https://www.google.com/maps/place/{}".format(
                score["facts"]["full_address"].replace(' ', '+'))
            areavibes_url = "https://www.areavibes.com/{}-{}/livability/".format(
                house['address_data']['city'].replace('-',
                                                      '+').replace(' ', '+'),
                house['address_data']['state'])
            spotcrime_url = "https://spotcrime.com/#{}".format(
                score["facts"]["full_address"].replace(' ', '%20').replace(
                    '-', '%20').replace(',', '%2C'))

            tabs = HouseScore.add_html_tab(
                tab_id="links_{}".format(house_group_id),
                tab_name="Links",
                tab_content="""
                <H3><A href="{}">Map</A></H3>
                <H3><A href="{}">Areavibes</A></H3>
                <H3><A href="{}">SpotCrime</A></H3>
                """.format(maps_url, areavibes_url, spotcrime_url),
                tabs=tabs)
            #
            #<IFRAME width='100%' height='500' src='https://spotcrime.com/"+spotcrime_sub_path+"'/>"
            tabs_html = "\n".join(tabs['tabs'] + tabs['contents'])
            this_house_report = r"""
        <TR {4}> <!-- draggable="true" //-->
            <TD align="center" valign="top" >
                <TABLE width="100%">
                    <TR><TD colspan="2" width="100%">
                        <H2><A href="http://www.redfin.com{1}">{2}</A></H2>
                    </TD></TR>
                    <TR><TD width="50%" align="center" valign="top" >
                        <A href="http://www.redfin.com{1}"><IMG width="100%" src="{0}" /></A><BR/>
                        <P>Details : {5}</P>
                    </TD><TD align="left" valign="top" width="50%">
                        {3}
                    </TD></TR>
                </TABLE>
            </TD>
        </TR>
                    """.format(
                score["facts"]["photo"]  #0
                ,
                house["URL"]  #1
                ,
                score["facts"]["full_address"]  #2
                ,
                tabs_html  #3
                ,
                ('class="one-house-dragable page-break"' if even_raw else
                 'class="one-house-dragable no-page-break"')  #4
                ,
                pass_message)

            html_content.append(this_house_report)

        if len(html_content) == 0:
            return ""

        category_template = """
        <H1>{$SUMMARY}</H1>
        <BR/>
        <TABLE width="90%" align="center" valign="top">
            {$ACCORDION_TEMPLATE_BODY}
        </TABLE>
        """

        category_template = category_template.replace("{$SUMMARY}", summary)
        category_template = category_template.replace(
            "{$ACCORDION_TEMPLATE_BODY}", "\n".join(html_content))

        return category_template

    @staticmethod
    def search_for_range(matches,
                         active_only=True,
                         min_price=0,
                         max_price=7600000,
                         only_good=True):
        good_houses = matches
        if active_only:
            good_houses = [
                m for m in good_houses
                if m.get('status') is None or m['status'] == 'Active'
            ]
        if only_good:
            good_houses = [
                m for m in good_houses if m['scores'].get("cutoff") is None
            ]
        good_houses = [
            m for m in good_houses
            if m['price'] >= min_price and m['price'] <= max_price
        ]
        sorted_good_houses = HouseScore.sort_by_total_score_vs_price(
            good_houses)
        return sorted_good_houses

    @staticmethod
    def get_houses_html(houses, title="", active_only=True, only_good=True):
        tabs = None
        filtered_matches = HouseScore.search_for_range(houses,
                                                       active_only=active_only,
                                                       min_price=490000,
                                                       max_price=760000,
                                                       only_good=only_good)
        tab_content = HouseScore.get_houses_category_html(
            filtered_matches,
            category_id="full",
            summary="Found ( {} / {} ) good houses".format(
                len(filtered_matches), len(houses)))
        tabs = HouseScore.add_html_tab(tab_id="full_id",
                                       tab_name="All Houses",
                                       tab_content=tab_content,
                                       tabs=tabs)

        filtered_matches = HouseScore.search_for_range(houses,
                                                       active_only=active_only,
                                                       min_price=490000,
                                                       max_price=610000,
                                                       only_good=only_good)
        tab_content = HouseScore.get_houses_category_html(
            filtered_matches,
            category_id="low",
            summary="Found ( {} / {} ) good houses".format(
                len(filtered_matches), len(houses)))
        tabs = HouseScore.add_html_tab(tab_id="low_id",
                                       tab_name="Low",
                                       tab_content=tab_content,
                                       tabs=tabs)

        filtered_matches = HouseScore.search_for_range(houses,
                                                       active_only=active_only,
                                                       min_price=590000,
                                                       max_price=710000,
                                                       only_good=only_good)
        tab_content = HouseScore.get_houses_category_html(
            filtered_matches,
            category_id="med",
            summary="Found ( {} / {} ) good houses".format(
                len(filtered_matches), len(houses)))
        tabs = HouseScore.add_html_tab(tab_id="med_id",
                                       tab_name="Medium",
                                       tab_content=tab_content,
                                       tabs=tabs)

        filtered_matches = HouseScore.search_for_range(houses,
                                                       active_only=active_only,
                                                       min_price=690000,
                                                       max_price=760000,
                                                       only_good=only_good)
        tab_content = HouseScore.get_houses_category_html(
            filtered_matches,
            category_id="high",
            summary="Found ( {} / {} ) good houses".format(
                len(filtered_matches), len(houses)))
        tabs = HouseScore.add_html_tab(tab_id="high_id",
                                       tab_name="High",
                                       tab_content=tab_content,
                                       tabs=tabs)

        with open(os.path.join(SCRIPT_DIR, "report_template.html"),
                  "r") as html_template_stream:
            html_template = html_template_stream.read()

        html_template = html_template.replace("{$TITLE}", title)
        html_template = html_template.replace("{$TABS}",
                                              "\n".join(tabs['tabs']))
        html_template = html_template.replace("{$TAB_CONTENTS}",
                                              "\n".join(tabs['contents']))

        return html_template

    @staticmethod
    def sort_by_total_score_vs_price(good_houses):
        cost_gain = {}
        for m in good_houses:
            gain = m['scores']['value']["money"] - m['scores']['facts']['price']
            gain_percentage = m['scores']['value']["value"]
            if cost_gain.get(gain_percentage) is None:
                cost_gain[gain_percentage] = []
            cost_gain[gain_percentage].append(m)

        sorted_keys = sorted(cost_gain, reverse=True)
        #logging.debug(sorted_keys)
        retVal = []
        for k in sorted_keys:
            for m in cost_gain[k]:
                #m['scores']['gain_percentage'] = k
                retVal.append(m)
        return retVal

    def SearchByUrl(self,
                    house_url,
                    get_details=True,
                    force=False,
                    cache_time_format="%Y%m%d"):
        house, details = self.rfapi.get_house_by_url(
            house_url, force=force, cache_time_format=cache_time_format)
        return self.post_process([house],
                                 "SearchByUrl({})".format(house_url),
                                 get_details=get_details,
                                 force=force)

    def Search(self, search_name, search_json, get_details=False, force=False):
        logging.debug(
            "Search(search_name={}, search_json={}, get_details={}, force={})".
            format(search_name, search_json, get_details, force))
        matches = self.rfapi.retrieve_json(search_json, force=force)
        return self.post_process(matches,
                                 search_name,
                                 get_details=get_details,
                                 force=force)

    def SearchForZIPCodes(self,
                          search_name,
                          search_json,
                          zip_codes,
                          get_details=False,
                          force=False):
        zip_regions = [
            self.rfapi.zipcode_to_regionid(zipcode, False)
            for zipcode in zip_codes
        ]
        # need to convert to region_id
        region_types = [2 for zipcode in zip_codes]
        all_matches = []
        for region in zip_regions:
            search_json["region_id"] = [region]
            matches = self.rfapi.retrieve_json(search_json, force=force)
        all_matches += matches
        return self.post_process(all_matches,
                                 search_name,
                                 get_details=get_details,
                                 force=force)

    @staticmethod
    def LoadFavorits(FavoriteLocationsFile):
        with open(FavoriteLocationsFile, "r") as stream:
            fav = json.load(stream)
            ret = []
            for v in fav:
                if v['importance'] != 0:
                    ret.append(v)
            return ret