Esempio n. 1
0
    def score_and_explain(self, project: scoring_base.ScoringProject) \
            -> scoring_base.ExplainedScore:
        """Compute a score for the given ScoringProject."""

        reasons: List[str] = []

        # For now we just match for people willing to move to the whole country.
        # There might be cases where we should be able to recommend to people who want to move to
        # their own region, but it would add complexity to find them.
        is_not_ready_to_move = project.details.area_type < geo_pb2.COUNTRY

        is_not_single = project.user_profile.family_situation != user_pb2.SINGLE
        has_advanced_degree = project.user_profile.highest_degree >= job_pb2.LICENCE_MAITRISE
        is_not_young = project.get_user_age() > 30
        looks_only_for_cdi = project.details.employment_types == [job_pb2.CDI]

        if (is_not_ready_to_move or is_not_young or is_not_single
                or has_advanced_degree or looks_only_for_cdi):
            return scoring_base.NULL_EXPLAINED_SCORE
        reasons.append(
            project.translate_string(
                'vous nous avez dit être prêt%eFeminine à déménager'))
        reasons.append(
            project.translate_string('vous êtes disponible familialement'))

        if len(self._get_seasonal_departements(project).departement_stats) > 1:
            reasons.append(
                project.translate_string(
                    "il y a plus d'offres saisonnières par habitants dans d'autres villes"
                ))
            return scoring_base.ExplainedScore(2, reasons)
        return scoring_base.NULL_EXPLAINED_SCORE
Esempio n. 2
0
 def score_and_explain(self, project: scoring_base.ScoringProject) \
         -> scoring_base.ExplainedScore:
     if project.details.diagnostic.category_id == 'enhance-methods-to-interview':
         return scoring_base.ExplainedScore(3, [])
     reasons = [
         project.translate_static_string(
             "vous nous avez dit avoir passé beaucoup d'entretiens sans succès"
         )
     ]
     if project.details.total_interview_count < 0:
         num_interviews = 0
     elif project.details.total_interview_count > 0:
         num_interviews = project.details.total_interview_count
     else:
         num_interviews = _NUM_INTERVIEWS.get(
             project.details.total_interviews_estimate, 0)
     job_search_length_months = project.get_search_length_at_creation()
     if job_search_length_months < 1:
         job_search_length_months = 1
     num_monthly_interviews = num_interviews / job_search_length_months
     if num_monthly_interviews > _max_monthly_interviews(project):
         return scoring_base.ExplainedScore(3, reasons)
     # Whatever the number of month of search, trigger 3 if the user did more than 5 interviews:
     if num_interviews >= _NUM_INTERVIEWS[project_pb2.DECENT_AMOUNT]:
         return scoring_base.ExplainedScore(3, reasons)
     if project.details.diagnostic.category_id == 'bravo':
         return scoring_base.ExplainedScore(1, [])
     return scoring_base.NULL_EXPLAINED_SCORE
Esempio n. 3
0
    def score(self, project: scoring_base.ScoringProject) -> float:
        application_modes = project.job_group_info().application_modes.values()

        missing_fields: Set[str] = set()
        # User's job has no application modes info.
        if not application_modes:
            rome_id = project.details.target_job.job_group.rome_id
            missing_fields.add(
                f'data.job_group_info.{rome_id}.application_modes')
            raise scoring_base.NotEnoughDataException(
                "User's job has no application modes info. We cannot say whether this is blocking.",
                fields=missing_fields)

        user_application_mode = project.details.preferred_application_mode
        if not user_application_mode:
            missing_fields.add('projects.0.preferredApplicationMode')
            raise scoring_base.NotEnoughDataException(
                "Missing some information about user's application modes",
                fields=missing_fields)

        first_modes = project.get_fap_modes()
        second_modes = project.get_fap_modes(rank='second')

        if user_application_mode in first_modes:
            # User uses the correct application mode.
            return 0

        if user_application_mode in second_modes:
            # User uses one of the best application modes.
            return 1

        return 3
Esempio n. 4
0
def _score_and_explain_after_filters(project: scoring_base.ScoringProject) \
        -> scoring_base.ExplainedScore:
    """A helper function to give a score and an explanation for all advices in the module,
    once some prerequisite filters have been met.
    """

    if project.user_profile.has_car_driving_license != boolean_pb2.FALSE:
        return scoring_base.NULL_EXPLAINED_SCORE
    reasons = []
    license_required = next(
        (license.percent_required
         for license in project.job_group_info().requirements.driving_licenses
         if license.driving_license == job_pb2.CAR), 0)
    if license_required:
        reasons.append(
            project.translate_static_string(
                'le permis est important dans votre métier'))
    score_modifier = 0
    if _license_helps_mobility(project.details):
        reasons.append(
            project.translate_static_string(
                'le permis augmenterait votre mobilité'))
        score_modifier = 1
    if not reasons:
        return scoring_base.NULL_EXPLAINED_SCORE
    score = min(
        3,
        score_modifier + (
            # Example at 80% is civil engineer F1106.
            3 if license_required > 80 else
            # Example at 67% is translator E1108.
            2 if license_required > 67 else
            # Example at 50% is chiropractor J1408.
            1 if license_required > 50 else 0))
    return scoring_base.ExplainedScore(score, reasons)
Esempio n. 5
0
 def score_relevance(self, project: scoring_base.ScoringProject) \
         -> diagnostic_pb2.CategoryRelevance:
     try:
         project.score('category-enhance-methods-to-interview')
     except scoring_base.NotEnoughDataException:
         return diagnostic_pb2.NEUTRAL_RELEVANCE
     return diagnostic_pb2.RELEVANT_AND_GOOD
Esempio n. 6
0
def _find_best_departements(unused_: Any, project: scoring_base.ScoringProject) \
        -> list[project_pb2.DepartementScore]:
    """Find which are the best departement to relocate for a given job group."""

    own_departement_offers = project.imt_proto(
    ).yearly_avg_offers_per_10_candidates

    # If we do not have data about our own departement, we choose not to say anything.
    if not own_departement_offers:
        return []

    best_departements = project.job_group_info().departement_scores

    result: list[project_pb2.DepartementScore] = []
    for dep in itertools.islice(best_departements, 10):
        if dep.local_stats.imt.yearly_avg_offers_per_10_candidates <= own_departement_offers:
            return result
        offer_ratio = \
            dep.local_stats.imt.yearly_avg_offers_per_10_candidates / own_departement_offers
        result.append(
            project_pb2.DepartementScore(name=project.translate_string(
                geo.get_departement_name(project.database,
                                         dep.departement_id)),
                                         offer_ratio=offer_ratio))

    return result
Esempio n. 7
0
    def score_and_explain(self, project: scoring_base.ScoringProject) \
            -> scoring_base.ExplainedScore:
        """Compute a score for the given ScoringProject."""

        close_jobs = self.get_close_jobs(project)
        search_since_nb_months = round(project.get_search_length_now())
        score_modifier = 0
        reasons: List[str] = []
        if len(close_jobs.close_jobs) + len(close_jobs.evolution_jobs) < 2:
            return scoring_base.NULL_EXPLAINED_SCORE
        # TODO(cyrille): Make this more robust.
        force_in_stuck_market = None
        # TODO(cyrille): Rather use market_stress to avoid depending on diagnostic to be computed.
        if project.details.diagnostic.category_id == 'stuck-market':
            force_in_stuck_market = scoring_base.ExplainedScore(1, reasons)
        if project.get_user_age() >= 45:
            return force_in_stuck_market or scoring_base.NULL_EXPLAINED_SCORE
        if project.details.passionate_level >= project_pb2.PASSIONATING_JOB:
            score_modifier = -1
        else:
            reasons.append(project.translate_string(
                "vous n'êtes pas trop attaché à votre métier"))
        if project.details.job_search_has_not_started or search_since_nb_months <= 1:
            return scoring_base.ExplainedScore(2 + score_modifier, reasons)
        reasons = [
            project.translate_string('vous cherchez depuis {} mois')
            .format(search_since_nb_months)]
        if search_since_nb_months >= 12:
            return scoring_base.ExplainedScore(3, reasons)
        if search_since_nb_months >= 9:
            return scoring_base.ExplainedScore(2, reasons)
        if search_since_nb_months >= 6:
            return scoring_base.ExplainedScore(1, reasons)
        return force_in_stuck_market or scoring_base.NULL_EXPLAINED_SCORE
Esempio n. 8
0
    def get_local_jobbing(self, project: scoring_base.ScoringProject) \
            -> reorient_jobbing_pb2.JobbingReorientJobs:
        """Get the jobbing opportunities for the departement."""

        recommended_jobs = reorient_jobbing_pb2.JobbingReorientJobs()
        departement_id = project.details.city.departement_id
        gender = project.user_profile.gender
        top_unqualified_jobs = self.list_reorient_jobbing_jobs(project)
        current_job_market_score = project.imt_proto(
        ).yearly_avg_offers_per_10_candidates
        if departement_id in top_unqualified_jobs:
            for job in top_unqualified_jobs[
                    departement_id].departement_job_stats.jobs:
                if current_job_market_score:
                    if job.market_score / current_job_market_score < 1 and \
                            not project.features_enabled.all_modules:
                        break
                    offers_gain = (
                        job.market_score / current_job_market_score - 1) * 100
                else:
                    if job.market_score < 8:
                        # User is in an unknown market, we only show jobbing jobs that have at least
                        # some job offers.
                        break
                    offers_gain = 0
                recommended_jobs.reorient_jobbing_jobs.add(
                    name=project.translate_string(
                        french.genderize_job(job, gender)),
                    offers_percent_gain=offers_gain)
        return recommended_jobs
Esempio n. 9
0
    def score_and_explain(self, project: scoring_base.ScoringProject) \
            -> scoring_base.ExplainedScore:
        """Compute a score for the given ScoringProject."""

        local_jobbing = self.get_local_jobbing(project)
        if len(local_jobbing.reorient_jobbing_jobs) < 2:
            return scoring_base.NULL_EXPLAINED_SCORE
        score_modifier = 0
        reasons: list[str] = []

        if project.details.passionate_level == project_pb2.LIFE_GOAL_JOB:
            score_modifier = -2
            if project.job_group_info().growth_2012_2022 < .1:
                score_modifier = -1
        if score_modifier >= 0:
            reasons.append(
                project.translate_static_string(
                    'votre métier ne vous tient pas trop à cœur'))

        if project.user_profile.highest_degree <= job_pb2.CAP_BEP:
            return scoring_base.ExplainedScore(3 + score_modifier, reasons)
        if project.user_profile.highest_degree <= job_pb2.BAC_BACPRO:
            return scoring_base.ExplainedScore(max(2 + score_modifier, 1),
                                               reasons)
        if project.user_profile.highest_degree <= job_pb2.BTS_DUT_DEUG:
            return scoring_base.ExplainedScore(1, reasons)
        return scoring_base.NULL_EXPLAINED_SCORE
Esempio n. 10
0
    def score_to_hundred(self, project: scoring_base.ScoringProject) -> float:
        """Compute a score for the given ScoringProject."""

        area_type = project.details.area_type

        if not area_type:
            raise scoring_base.NotEnoughDataException()

        if not project.imt_proto().yearly_avg_offers_per_10_candidates:
            raise scoring_base.NotEnoughDataException()

        num_better_departements = project.local_diagnosis(
        ).num_less_stressful_departements

        # User is already in one of the top 3 départements.
        if num_better_departements < 3:
            raise scoring_base.NotEnoughDataException()

        # Give a score centered around 50 corresponding to the user's mobility.
        # The worse the current département is, the more extreme we set the
        # score.

        score_range = min(num_better_departements * 1.5, 100) / 2

        if area_type >= geo_pb2.COUNTRY:
            return 50 + score_range

        if area_type >= geo_pb2.REGION:
            return 50

        if area_type >= geo_pb2.DEPARTEMENT:
            return 50 - score_range / 2

        return 50 - score_range
Esempio n. 11
0
    def score_and_explain(self, project: scoring_base.ScoringProject) \
            -> scoring_base.ExplainedScore:
        """Compute the score for a given project and explains it.

        Requirements are:
        - being between 16 and 30 y.o if having a handicap or between 16 and 25 otherwise
        - having low or no experience (intern maximum)
        """

        age = project.get_user_age()
        seniority = project.details.seniority
        reasons: list[str] = []
        if age < 16 or seniority > project_pb2.INTERN:
            return scoring_base.NULL_EXPLAINED_SCORE
        if project.user_profile.has_handicap and age <= 30:
            reasons = [
                project.translate_static_string('vous avez entre 16 et 30 ans')
            ]
        if age <= 25:
            reasons = [
                project.translate_static_string('vous avez entre 16 et 25 ans')
            ]
        if not reasons:
            return scoring_base.NULL_EXPLAINED_SCORE
        return scoring_base.ExplainedScore(2, reasons)
Esempio n. 12
0
    def score_and_explain(self, project: scoring_base.ScoringProject) \
            -> scoring_base.ExplainedScore:
        """Compute a score for the given ScoringProject."""

        associations = self.list_associations(project)
        search_length_reason = project.translate_static_string(
            "vous nous avez dit que vous êtes en recherche d'emploi "
            'depuis %jobSearchLengthAtCreation')
        if not associations:
            return scoring_base.NULL_EXPLAINED_SCORE
        if user_profile_pb2.MOTIVATION in project.user_profile.frustrations:
            return scoring_base.ExplainedScore(3, [
                project.translate_static_string(
                    'vous nous avez dit avoir du mal à garder votre ' +
                    'motivation au top')
            ])
        if len(associations) >= 3 and project.get_search_length_at_creation(
        ) >= 6:
            return scoring_base.ExplainedScore(3, [search_length_reason])
        if project.get_search_length_at_creation() >= 12:
            return scoring_base.ExplainedScore(3, [search_length_reason])
        return scoring_base.ExplainedScore(2, [
            project.translate_static_string(
                "l'accompagnement humain peut beaucoup vous apporter")
        ])
Esempio n. 13
0
def _in_region(scoring_project: scoring_base.ScoringProject) -> str:
    region = scoring_project.get_region()
    if not region or not region.name:
        return scoring_project.translate_static_string('dans la région')
    if not region.prefix:
        return scoring_project.translate_static_string('en {region_name}')\
            .format(region_name=region.name)
    return region.prefix + region.name
Esempio n. 14
0
def _an_application_mode(scoring_project: scoring_base.ScoringProject) -> str:
    best_mode = scoring_project.get_best_application_mode()
    best_mode_enum = best_mode.mode if best_mode else job_pb2.PERSONAL_OR_PROFESSIONAL_CONTACTS
    try:
        application_mode_in_french = APPLICATION_MODES[best_mode_enum]
    except KeyError:
        application_mode_in_french = APPLICATION_MODES[
            job_pb2.PERSONAL_OR_PROFESSIONAL_CONTACTS]
    return scoring_project.translate_static_string(application_mode_in_french)
Esempio n. 15
0
def _a_required_diploma(scoring_project: scoring_base.ScoringProject) -> str:
    diplomas = scoring_project.requirements().diplomas
    if not diplomas:
        logging.warning(
            'Trying to show required diplomas when there are none.\n%s',
            str(scoring_project))
        return scoring_project.translate_static_string('un diplôme')
    return scoring_project.translate_static_string(
        'un {diplomas} ou équivalent').format(diplomas=diplomas[0].name)
Esempio n. 16
0
    def score(self, project: scoring_base.ScoringProject) -> float:
        # TODO(cyrille): Find a way to make sure they're really desillusioned.
        if project.get_search_length_now() < 3:
            # User is probably not disillusioned yet.
            return 0
        if project.details.seniority < project_pb2.CARREER:
            return 0

        return max(0, min(3, project.get_user_age() - 50))
Esempio n. 17
0
    def score_and_explain(self, project: scoring_base.ScoringProject) \
            -> scoring_base.ExplainedScore:
        """Compute a score for the given ScoringProject."""

        frustration_reasons = list(self._get_frustrations_reasons(project))
        its_easy = project.translate_static_string(
            "c'est plus facile à faire qu'on peut le croire")

        if frustration_reasons or project.get_search_length_now() > 3:
            return scoring_base.ExplainedScore(
                2, frustration_reasons or [its_easy])
        return scoring_base.ExplainedScore(1, [its_easy])
Esempio n. 18
0
    def score(self, project: scoring_base.ScoringProject) -> float:
        """Compute a score for the given ScoringProject."""

        lang_requirements = _LANGUAGE_REQUIREMENTS.get(project.details.city.departement_id)
        if not lang_requirements:
            # Language is not relevant for this city.
            return 0

        try:
            project.score('for-missing-language')
        except scoring_base.NotEnoughDataException:
            return 1
        return 3
Esempio n. 19
0
    def score_and_explain(self, project: scoring_base.ScoringProject) \
            -> scoring_base.ExplainedScore:
        """Compute a score for the given ScoringProject."""

        if (self._num_interviews_increase(project) >= 2 and
                project.details.job_search_length_months <= 6):
            return scoring_base.ExplainedScore(3, [project.translate_string(
                "nous pensons qu'avec votre profil vous pourriez "
                "décrocher plus d'entretiens")])
        if project.details.diagnostic.category_id == 'bravo' and \
                user_pb2.RESUME in project.user_profile.frustrations:
            return scoring_base.ExplainedScore(1, [project.translate_string(
                'vous nous avez dit avoir du mal à rédiger votre CV')])
        return scoring_base.NULL_EXPLAINED_SCORE
Esempio n. 20
0
    def _num_interviews_increase(self, project: scoring_base.ScoringProject) -> float:
        """Compute the increase (in ratio) of # of interviews that one could hope for."""

        if project.details.total_interviews_estimate >= project_pb2.A_LOT or \
                project.details.total_interview_count > 20:
            return 0

        job_search_length_weeks = project.get_search_length_at_creation() * 52 / 12
        num_applicants_per_offer = project.market_stress() or 2.85
        weekly_applications = _APPLICATION_PER_WEEK.get(
            project.details.weekly_applications_estimate, 0)
        num_applications = job_search_length_weeks * weekly_applications
        num_potential_interviews = num_applications / num_applicants_per_offer
        return num_potential_interviews / (self._num_interviews(project) or 1)
Esempio n. 21
0
    def score_and_explain(self, project: scoring_base.ScoringProject) \
            -> scoring_base.ExplainedScore:
        """Compute a score for the given ScoringProject."""

        if project.details.weekly_applications_estimate <= project_pb2.LESS_THAN_2 or \
                project.details.job_search_length_months < 2:
            return scoring_base.ExplainedScore(3, [project.translate_string(
                'vous nous avez dit que vous en êtes au début de '
                'vos candidatures')])
        if project.details.diagnostic.category_id == 'bravo' and \
                user_pb2.RESUME in project.user_profile.frustrations:
            return scoring_base.ExplainedScore(1, [project.translate_string(
                'vous nous avez dit avoir du mal à rédiger votre CV')])

        return scoring_base.NULL_EXPLAINED_SCORE
Esempio n. 22
0
    def get_expanded_card_data(self, project: scoring_base.ScoringProject) \
            -> network_pb2.ContactLeads:
        """Retrieve data for the expanded card."""

        contact_leads = self._list_contact_leads(project)
        sorted_leads = sorted(contact_leads,
                              key=lambda l: (-len(l.filters), random.random()))
        return network_pb2.ContactLeads(leads=[
            network_pb2.ContactLead(
                name=project.populate_template(
                    project.translate_string(template.name)),
                email_example=project.populate_template(
                    project.translate_string(template.email_template)),
                contact_tip=project.translate_string(template.contact_tip))
            for template in sorted_leads
        ])
Esempio n. 23
0
    def score(self, project: scoring_base.ScoringProject) -> float:
        job_search_length = project.get_search_length_now()

        # User has not started their job search.
        if job_search_length <= 0:
            raise scoring_base.NotEnoughDataException(
                'Search not started yet. We cannot say whether this is blocking.')

        missing_fields: Set[str] = set()
        if not project.details.total_interview_count:
            missing_fields.add('projects.0.totalInterviewCount')
        if not project.details.weekly_applications_estimate:
            missing_fields.add('projects.0.weeklyApplicationsEstimate')

        # Either negative or between 0 and 1.
        interview_score = 1 - max(0, project.details.total_interview_count) / job_search_length
        if interview_score <= 0:
            # User has at least one interview per month, they don't need more tips on getting
            # interviews.
            return 0

        if missing_fields:
            raise scoring_base.NotEnoughDataException(
                'Missing some information about applications', fields=missing_fields)

        # Varies between 0 and 3.
        application_score = _APPLICATION_PER_WEEK[project.details.weekly_applications_estimate] / 5

        # Varies between 0 and 3.
        return interview_score * application_score
 def _convert_to_reorient_jobs(
     self, database: mongo.NoPiiMongoDatabase,
     reorient_jobs: Iterable[job_pb2.RelatedJobGroup],
     market_score_source: float, project: scoring_base.ScoringProject
 ) -> Iterator[reorient_jobbing_pb2.ReorientJob]:
     for job in reorient_jobs:
         # Here the market score improvement
         # (job that the user is searching for vs recommended job)
         # is overly simplified as offers gain.
         # TODO(sil): Find a way to explain the market score improvement to the user.
         # TODO(cyrille): Replace offers_percent_gain by stress_percent_loss to simplify
         #   client-side computations.
         offers_gain = 100 * (
             job.local_stats.imt.yearly_avg_offers_per_10_candidates /
             market_score_source - 1)
         job_group_info = jobs.get_group_proto(database,
                                               job.job_group.rome_id,
                                               project.user_profile.locale)
         is_diploma_required = False
         if job_group_info:
             is_diploma_required = job_group_info.is_diploma_strictly_required
         yield reorient_jobbing_pb2.ReorientJob(
             name=job_group_info and job_group_info.name
             or project.translate_string(job.job_group.name),
             offers_percent_gain=offers_gain,
             is_diploma_strictly_required=is_diploma_required)
    def get_close_jobs(self, project: scoring_base.ScoringProject) \
            -> reorient_to_close_pb2.ReorientCloseJobs:
        """Get the jobs close to a job group."""

        recommended_jobs = reorient_to_close_pb2.ReorientCloseJobs()
        local_diagnosis = project.local_diagnosis()
        market_score = local_diagnosis.imt.yearly_avg_offers_per_10_candidates
        if not market_score:
            return recommended_jobs
        reorientation_jobs = local_diagnosis.less_stressful_job_groups
        close_jobs = [
            job for job in reorientation_jobs
            if job.mobility_type == job_pb2.CLOSE
        ]
        evolution_jobs = [
            job for job in reorientation_jobs
            if job.mobility_type == job_pb2.EVOLUTION
        ]
        database = project.database
        recommended_jobs.close_jobs.extend(
            self._convert_to_reorient_jobs(database, close_jobs, market_score,
                                           project))
        recommended_jobs.evolution_jobs.extend(
            self._convert_to_reorient_jobs(database, evolution_jobs,
                                           market_score, project))
        return recommended_jobs
Esempio n. 26
0
def _max_monthly_interviews(project: scoring_base.ScoringProject) -> int:
    """Maximum number of monthly interviews one should have."""

    if project.job_group_info(
    ).application_complexity == job_pb2.COMPLEX_APPLICATION_PROCESS:
        return 5
    return 3
Esempio n. 27
0
 def score_to_hundred(self, project: scoring_base.ScoringProject) -> float:
     market_stress = project.market_stress()
     if not market_stress:
         raise scoring_base.NotEnoughDataException()
     if market_stress == 1000:
         return 0
     return 1 / market_stress * 100
Esempio n. 28
0
    def score_to_hundred(self, project: scoring_base.ScoringProject) -> float:
        """Compute a percentage score for the given ScoringProject."""

        growth_2012_2022 = project.job_group_info().growth_2012_2022
        if not growth_2012_2022:
            raise scoring_base.NotEnoughDataException()
        return _interpolate_points(growth_2012_2022, [(-.17, 0), (.07, 50),
                                                      (.29, 100)])
Esempio n. 29
0
    def score_and_explain(self, project: scoring_base.ScoringProject) \
            -> scoring_base.ExplainedScore:
        """Compute a score for the given ScoringProject."""

        first_modes = project.get_fap_modes()
        first_modes.discard(job_pb2.UNDEFINED_APPLICATION_MODE)
        if first_modes == {job_pb2.PERSONAL_OR_PROFESSIONAL_CONTACTS}:
            return scoring_base.ExplainedScore(2, [
                project.translate_static_string(
                    'les embauches se font surtout par les contacts personnels ou professionnels dans'
                    ' votre métier')
            ])

        return scoring_base.ExplainedScore(1, [
            project.translate_static_string(
                "c'est un bon moyen d'étendre votre réseau")
        ])
Esempio n. 30
0
    def score_and_explain(self, project: scoring_base.ScoringProject) \
            -> scoring_base.ExplainedScore:
        """Compute the score for a given project and explains it."""

        age = project.get_user_age()
        if age < 16 or age >= 25:
            return scoring_base.NULL_EXPLAINED_SCORE
        return _score_and_explain_after_filters(project)