package uk.ac.ox.zoo.seeg.abraid.mp.common.service.workflow.support;
import ch.lambdaj.function.convert.Converter;
import org.apache.log4j.Logger;
import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.*;
import uk.ac.ox.zoo.seeg.abraid.mp.common.service.core.DiseaseService;
import uk.ac.ox.zoo.seeg.abraid.mp.common.service.core.ExpertService;
import java.util.*;
import static ch.lambdaj.Lambda.*;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.hamcrest.core.IsEqual.equalTo;
/**
* Updates the weightings of experts and of disease occurrences, given new reviews.
* Copyright (c) 2014 University of Oxford
*/
public class WeightingsCalculator {
private static Logger logger = Logger.getLogger(WeightingsCalculator.class);
private static final String NOT_UPDATING_OCCURRENCE_EXPERT_WEIGHTINGS =
"No new occurrence reviews have been submitted by experts with a weighting >= %.2f - " +
"expert weightings of disease occurrences will not be updated";
private static final String RECALCULATING_OCCURRENCE_EXPERT_WEIGHTINGS =
"Recalculating expert weightings for %d disease occurrence(s) given %d new review(s)";
private static final String NOT_UPDATING_WEIGHTINGS_OF_EXPERTS =
"No occurrence reviews have been submitted - weightings of experts will not be updated";
private static final String RECALCULATING_WEIGHTINGS_OF_EXPERTS =
"Recalculating weightings of experts given %d review(s)";
private static final String NO_OCCURRENCES_FOR_MODEL_RUN =
"No occurrences found that need their validation and final weightings set";
private static final String UPDATING_WEIGHTINGS =
"Updating validation and final weightings for %d disease occurrence(s) in preparation for model run";
private final double validationWeightingThreshold;
private final double expertWeightingThreshold;
private DiseaseService diseaseService;
private ExpertService expertService;
public WeightingsCalculator(DiseaseService diseaseService, ExpertService expertService,
double expertWeightingThreshold, double validationWeightingThreshold) {
this.diseaseService = diseaseService;
this.expertService = expertService;
this.expertWeightingThreshold = expertWeightingThreshold;
this.validationWeightingThreshold = validationWeightingThreshold;
}
/**
* Calculate and save the new weighting of disease occurrence points in validation that have had reviews submitted:
* Take the average response of the reviews from the "reliable" experts (those who have an expert weighting greater
* than EXPERT_WEIGHTING_THRESHOLD).
* @param diseaseGroupId The id of the disease group.
*/
public void updateDiseaseOccurrenceExpertWeightings(int diseaseGroupId) {
List<DiseaseOccurrenceReview> allReviews =
diseaseService.getDiseaseOccurrenceReviewsForOccurrencesInValidationForUpdatingWeightings(
diseaseGroupId, expertWeightingThreshold);
if (allReviews.isEmpty()) {
logger.info(String.format(NOT_UPDATING_OCCURRENCE_EXPERT_WEIGHTINGS, expertWeightingThreshold));
} else {
updateDiseaseOccurrenceExpertWeightings(allReviews);
}
}
private void updateDiseaseOccurrenceExpertWeightings(List<DiseaseOccurrenceReview> allReviews) {
for (DiseaseOccurrence occurrence : extractDistinctDiseaseOccurrences(allReviews)) {
List<DiseaseOccurrenceReview> reviews = selectReviewsForOccurrence(allReviews, occurrence);
double averageResponseValue = average(extractReviewResponseValues(reviews));
occurrence.setExpertWeighting(averageResponseValue);
diseaseService.saveDiseaseOccurrence(occurrence);
}
}
private Set<DiseaseOccurrence> extractDistinctDiseaseOccurrences(List<DiseaseOccurrenceReview> allReviews) {
Set<DiseaseOccurrence> occurrences = new HashSet<>(
extract(allReviews, on(DiseaseOccurrenceReview.class).getDiseaseOccurrence())
);
logger.info(String.format(RECALCULATING_OCCURRENCE_EXPERT_WEIGHTINGS, occurrences.size(), allReviews.size()));
return occurrences;
}
private List<DiseaseOccurrenceReview> selectReviewsForOccurrence(List<DiseaseOccurrenceReview> reviews,
DiseaseOccurrence occurrence) {
return select(reviews, having(on(DiseaseOccurrenceReview.class).getDiseaseOccurrence(), equalTo(occurrence)));
}
private List<Double> extractReviewResponseValues(List<DiseaseOccurrenceReview> reviewsOfOccurrence) {
return convert(reviewsOfOccurrence, new Converter<DiseaseOccurrenceReview, Double>() {
public Double convert(DiseaseOccurrenceReview review) {
return review.getResponse().getValue();
}
});
}
/**
* For every occurrence of the specified disease group for which the status is READY, and the final weighting is
* not currently set, set its validation weighting and final weighting for the first and only time.
* @param diseaseGroupId The id of the disease group.
*/
public void updateDiseaseOccurrenceValidationWeightingAndFinalWeightings(int diseaseGroupId) {
List<DiseaseOccurrence> occurrences =
diseaseService.getDiseaseOccurrencesYetToHaveFinalWeightingAssigned(
diseaseGroupId, DiseaseOccurrenceStatus.READY);
if (occurrences.size() == 0) {
logger.info(NO_OCCURRENCES_FOR_MODEL_RUN);
} else {
logger.info(String.format(UPDATING_WEIGHTINGS, occurrences.size()));
updateDiseaseOccurrenceValidationWeightingAndFinalWeightings(occurrences);
}
}
private void updateDiseaseOccurrenceValidationWeightingAndFinalWeightings(List<DiseaseOccurrence> occurrences) {
for (DiseaseOccurrence occurrence : occurrences) {
Double newValidation = calculateNewValidationWeighting(occurrence);
double newFinal = calculateNewFinalWeighting(occurrence, newValidation);
double newFinalExcludingSpatial = calculateNewFinalWeightingExcludingSpatial(newValidation);
if (hasAnyWeightingChanged(occurrence, newValidation, newFinal, newFinalExcludingSpatial)) {
occurrence.setValidationWeighting(newValidation);
occurrence.setFinalWeighting(newFinal);
occurrence.setFinalWeightingExcludingSpatial(newFinalExcludingSpatial);
diseaseService.saveDiseaseOccurrence(occurrence);
}
}
}
/**
* Set the occurrence's validation weighting as the expert weighting if it exists, otherwise the machine weighting.
*/
private Double calculateNewValidationWeighting(DiseaseOccurrence occurrence) {
Double expertWeighting = occurrence.getExpertWeighting();
Double machineWeighting = occurrence.getMachineWeighting();
return (expertWeighting != null) ? expertWeighting : machineWeighting;
}
/**
* If the validation weighting is null, there are no reviews on the disease occurrence and no machine weighting
* either. So set the final weighting, nominally, to the location resolution weighting. (This happens during disease
* group setup, when the model has not yet been run.)
* Otherwise, recalculate the final weighting as the average of location resolution and validation weightings,
* unless the location resolution weighting is 0.
*/
private double calculateNewFinalWeighting(DiseaseOccurrence occurrence, Double validationWeighting) {
double locationResolutionWeighting = occurrence.getLocation().getResolutionWeighting();
if (validationWeighting == null) {
return locationResolutionWeighting;
} else if ((validationWeighting <= validationWeightingThreshold) || (locationResolutionWeighting == 0.0)) {
return 0.0;
} else {
return average(locationResolutionWeighting, validationWeighting);
}
}
/**
* As above, but excluding the location resolution weighting.
* In this case, if validation weighting is null, final weighting is 1.0.
*/
private double calculateNewFinalWeightingExcludingSpatial(Double validationWeighting) {
return (validationWeighting == null) ? 1.0 : validationWeighting;
}
private boolean hasAnyWeightingChanged(DiseaseOccurrence occurrence,
Double newValidation, double newFinal, double newFinalExcludingSpatial) {
return (hasWeightingChanged(occurrence.getValidationWeighting(), newValidation) ||
hasWeightingChanged(occurrence.getFinalWeighting(), newFinal) ||
hasWeightingChanged(occurrence.getFinalWeightingExcludingSpatial(), newFinalExcludingSpatial));
}
private boolean hasWeightingChanged(Double currentWeighting, Double newWeighting) {
return (currentWeighting == null || !currentWeighting.equals(newWeighting));
}
/**
* For each expert, calculate (and save) their new weighting as the absolute difference between their response and
* the average response from all other experts, averaged over all the occurrences that they have reviewed.
*/
public void updateExpertsWeightings() {
List<DiseaseOccurrenceReview> allReviews = diseaseService.getAllDiseaseOccurrenceReviews();
List<DiseaseOccurrenceReview> reviews = filter(
having(on(DiseaseOccurrenceReview.class).getResponse(), notNullValue()), allReviews);
if (reviews.size() == 0) {
logger.info(NOT_UPDATING_WEIGHTINGS_OF_EXPERTS);
} else {
logger.info(String.format(RECALCULATING_WEIGHTINGS_OF_EXPERTS, reviews.size()));
updateExpertsWeightings(reviews);
}
}
private void updateExpertsWeightings(List<DiseaseOccurrenceReview> allReviews) {
for (Expert expert : extractDistinctExperts(allReviews)) {
List<Double> differencesInResponses = new ArrayList<>();
for (DiseaseOccurrence occurrence : selectExpertsReviewedOccurrences(allReviews, expert)) {
differencesInResponses.add(calculateDifference(allReviews, occurrence, expert));
}
double newWeighting = 1 - average(differencesInResponses);
if (hasWeightingChanged(expert.getWeighting(), newWeighting)) {
expert.setWeighting(newWeighting);
expertService.saveExpert(expert);
}
}
}
private Set<Expert> extractDistinctExperts(List<DiseaseOccurrenceReview> allReviews) {
return new HashSet<>(extract(allReviews, on(DiseaseOccurrenceReview.class).getExpert()));
}
private Set<DiseaseOccurrence> selectExpertsReviewedOccurrences(List<DiseaseOccurrenceReview> reviews,
Expert expert) {
List<DiseaseOccurrenceReview> expertsReviews = select(reviews,
having(on(DiseaseOccurrenceReview.class).getExpert(), equalTo(expert)));
return new HashSet<>(extract(expertsReviews, on(DiseaseOccurrenceReview.class).getDiseaseOccurrence()));
}
private Double calculateDifference(List<DiseaseOccurrenceReview> reviews, DiseaseOccurrence occurrence,
Expert expert) {
List<DiseaseOccurrenceReview> reviewsOfOccurrence = select(reviews,
having(on(DiseaseOccurrenceReview.class).getDiseaseOccurrence(), equalTo(occurrence)));
DiseaseOccurrenceReview expertsReview = selectUnique(reviewsOfOccurrence,
having(on(DiseaseOccurrenceReview.class).getExpert(), equalTo(expert)));
reviewsOfOccurrence.remove(expertsReview);
double expertsResponse = expertsReview.getResponse().getValue();
if (reviewsOfOccurrence.size() > 0) {
// For this occurrence, find the difference between this expert's review response and the average of
// all other experts' review responses
double averageResponse = average(extractReviewResponseValues(reviewsOfOccurrence));
return Math.abs(expertsResponse - averageResponse);
} else {
// There are no other experts' review responses, so the difference is zero
return 0.0;
}
}
/**
* Calculate the average of the provided values.
* @param args The values.
* @return The mean of the given values.
*/
static double average(Double... args) {
return average(Arrays.asList(args));
}
private static double average(List<Double> args) {
List<Double> notNullValues = filter(notNullValue(), args);
return (double) avg(notNullValues);
}
}