OntologyMeasures.java
001 /**
002  *  Copyright (c) 1995-2012, The University of Sheffield. See the file
003  *  COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
004  *
005  *  This file is part of GATE (see http://gate.ac.uk/), and is free
006  *  software, licenced under the GNU Library General Public License,
007  *  Version 2, June 1991 (in the distribution as file licence.html,
008  *  and also available at http://gate.ac.uk/gate/licence.html).
009  *
010  * Thomas Heitz - 09/06/2010
011  *
012  *  $Id$
013  */
014 
015 package gate.util;
016 
017 import gate.Annotation;
018 
019 import java.io.BufferedReader;
020 import java.io.FileInputStream;
021 import java.io.IOException;
022 import java.net.URL;
023 import java.text.NumberFormat;
024 import java.util.*;
025 
026 /**
027  * Modified version of Precision and Recall called BDM that takes into
028  * account the distance of two concepts in an ontology.
029  */
030 public class OntologyMeasures {
031 
032   public OntologyMeasures() {
033     // empty constructor
034   }
035 
036   /**
037    * Constructor to be used when you have a collection of OntologyMeasures
038    * and want to consider it as only one OntologyMeasures.
039    * Then you can only use the methods getPrecision/Recall/FMeasure...().
040    @param measures collection to be regrouped in one OntologyMeasures
041    */
042   public OntologyMeasures(Collection<OntologyMeasures> measures) {
043     Map<String, List<AnnotationDiffer>> differsByTypeMap =
044       new HashMap<String, List<AnnotationDiffer>>();
045     for (OntologyMeasures measure : measures) {
046       for (Map.Entry<String, Float> entry : measure.bdmByTypeMap.entrySet()) {
047         float previousBdm = 0;
048         if (bdmByTypeMap.containsKey(entry.getKey())) {
049           previousBdm = bdmByTypeMap.get(entry.getKey());
050         }
051         // set the bdmByTypeMap to be the sum of those in the collection
052         bdmByTypeMap.put(entry.getKey(), previousBdm + entry.getValue());
053       }
054       for (Map.Entry<String, AnnotationDiffer> entry :
055              measure.differByTypeMap.entrySet()) {
056         List<AnnotationDiffer> differs = differsByTypeMap.get(entry.getKey());
057         if (differs == null) {
058           differs = new ArrayList<AnnotationDiffer>();
059         }
060         differs.add(entry.getValue());
061         differsByTypeMap.put(entry.getKey(), differs);
062       }
063     }
064     // combine the list of AnnotationDiffer for each type
065     for (Map.Entry<String, List<AnnotationDiffer>> entry :
066            differsByTypeMap.entrySet()) {
067       differByTypeMap.put(entry.getKey(),
068         new AnnotationDiffer(entry.getValue()));
069     }
070   }
071 
072   /**
073    * For a document get the annotation differs that contain the type to compare
074    * and the annotation differs that may have miscategorized annotations
075    * for this type. Then we try to find miscategorized types that are close
076    * enough from the main type and use their BDM value to get an augmented
077    * precision, recall and fscore.
078    *
079    @param differs annotation differ for the type and for possible
080    * miscategorized types.
081    */
082   public void calculateBdm(Collection<AnnotationDiffer> differs) {
083 
084     if (bdmByConceptsMap == null) {
085       // load BDM file with scores for each concept/annotation type pair
086       bdmByConceptsMap = read(bdmFileUrl)// read the bdm scores
087     }
088 
089     // calculate BDM from the spurious and missing annotations
090     Set<Annotation> unpairedResponseAnnotations = new HashSet<Annotation>();
091     Set<Annotation> unpairedKeyAnnotations;
092 
093     // will use the whole spurious annotations as the second set to compare
094     for (AnnotationDiffer differ : differs) {
095       unpairedResponseAnnotations.addAll(
096         differ.getAnnotationsOfType(AnnotationDiffer.SPURIOUS_TYPE));
097     }
098 
099     bdmByTypeMap.clear();
100 
101     for (AnnotationDiffer differ : differs) {
102       unpairedKeyAnnotations = differ.getAnnotationsOfType(
103         AnnotationDiffer.MISSING_TYPE);
104       if (!bdmByTypeMap.containsKey(differ.getAnnotationType())) {
105         bdmByTypeMap.put(differ.getAnnotationType()0f);
106       }
107 
108       // use the missing annotations as the first set to compare
109       for (Annotation unpairedKeyAnnotation : unpairedKeyAnnotations) {
110         String type = unpairedKeyAnnotation.getType();
111 //        Out.prln("unpairedKeyAnnotation: " + unpairedKeyAnnotation.toString());
112         Iterator<Annotation> iterator = unpairedResponseAnnotations.iterator();
113 
114         // use the spurious annotations as the second set to compare
115         while (iterator.hasNext()) {
116           Annotation unpairedResponseAnnotation = iterator.next();
117 //          Out.prln("unpairedResponsAnnotation: "
118 //            + unpairedResponseAnnotation.toString());
119           float bdm = 0;
120 
121           // annotations have the same start and end offsets
122           if (unpairedKeyAnnotation.coextensive(unpairedResponseAnnotation)) {
123 
124             // compare both features values with BDM pairs
125             if (differ.getSignificantFeaturesSet() != null) {
126               if (!type.equals(unpairedResponseAnnotation.getType())) {
127                 continue// types must be the same
128               }
129               for (Object feature : differ.getSignificantFeaturesSet()) {
130                 if (unpairedKeyAnnotation.getFeatures() == null
131                  || unpairedResponseAnnotation.getFeatures() == null) {
132                   continue;
133                 }
134 //                Out.prln("Feature: " + feature);
135                 String keyLabel = (String)
136                   unpairedKeyAnnotation.getFeatures().get(feature);
137 //                Out.prln("KeyLabel: " + keyLabel);
138                 String responseLabel = (String)
139                   unpairedResponseAnnotation.getFeatures().get(feature);
140 //                Out.prln("ResponseLabel: " + responseLabel);
141                 if (keyLabel == null || responseLabel == null) {
142                   // do nothing
143                 else if (bdmByConceptsMap.containsKey(
144                                               keyLabel + ", " + responseLabel)) {
145                   bdm += bdmByConceptsMap.get(keyLabel + ", " + responseLabel);
146                 else if (bdmByConceptsMap.containsKey(
147                                               responseLabel + ", " + keyLabel)) {
148                   bdm += bdmByConceptsMap.get(responseLabel + ", " + keyLabel);
149                 }
150               }
151               bdm = bdm / differ.getSignificantFeaturesSet().size();
152 
153             else // compare both types with BDM pairs
154               if (bdmByConceptsMap.containsKey(
155                     type + ',' + unpairedResponseAnnotation.getType())) {
156                 bdm = bdmByConceptsMap.get(
157                     type + ',' + unpairedResponseAnnotation.getType());
158               else if (bdmByConceptsMap.containsKey(
159                            unpairedResponseAnnotation.getType() ", " + type)) {
160                 bdm = bdmByConceptsMap.get(
161                            unpairedResponseAnnotation.getType() ", " + type);
162               }
163             }
164             if (bdm > 0) {
165               bdmByTypeMap.put(type, bdmByTypeMap.get(type+ bdm);
166               iterator.remove();
167 //              Out.prln("BDM: " + bdmByTypeMap.get(type));
168             }
169           }
170         }
171       }
172     }
173 
174     differByTypeMap.clear();
175     Map<String, List<AnnotationDiffer>> differsByTypeMap =
176       new HashMap<String, List<AnnotationDiffer>>();
177 
178     for (AnnotationDiffer differ : differs) {
179       // we consider that all annotations in AnnotationDiffer are the same type
180       String type = differ.getAnnotationType();
181       List<AnnotationDiffer> differsType = differsByTypeMap.get(type);
182       if (differsType == null) {
183         differsType = new ArrayList<AnnotationDiffer>();
184       }
185       differsType.add(differ);
186       differsByTypeMap.put(type, differsType);
187     }
188 
189     // combine the list of AnnotationDiffer for each type
190     for (Map.Entry<String, List<AnnotationDiffer>> entry :
191           differsByTypeMap.entrySet()) {
192       differByTypeMap.put(entry.getKey(),
193         new AnnotationDiffer(entry.getValue()));
194     }
195   }
196 
197   /**
198    * AP = (sum of BDMs for BDM-matching pair spurious/missing + Correct)
199    *    / (Correct + Spurious)
200    @param type annotation type
201    @return strict precision with BDM correction
202    */
203   public double getPrecisionStrictBdm(String type) {
204     AnnotationDiffer differ = differByTypeMap.get(type);
205     if (differ.getCorrectMatches() + differ.getSpurious() == 0) {
206       return 1.0;
207     }
208     return (bdmByTypeMap.get(type+ differ.getCorrectMatches())
209          (differ.getCorrectMatches() + differ.getSpurious());
210   }
211 
212   public double getPrecisionStrictBdm() {
213     double result = 0;
214     for (String type : differByTypeMap.keySet()) {
215       result += getPrecisionStrictBdm(type);
216     }
217     return result / differByTypeMap.size();
218   }
219 
220   public double getRecallStrictBdm(String type) {
221     AnnotationDiffer differ = differByTypeMap.get(type);
222     if (differ.getCorrectMatches() + differ.getMissing() == 0) {
223       return 1.0;
224     }
225     return (bdmByTypeMap.get(type+ differ.getCorrectMatches())
226          (differ.getCorrectMatches() + differ.getMissing());
227   }
228 
229   public double getRecallStrictBdm() {
230     double result = 0;
231     for (String type : differByTypeMap.keySet()) {
232       result += getRecallStrictBdm(type);
233     }
234     return result / differByTypeMap.size();
235   }
236 
237   public double getFMeasureStrictBdm(String type, double beta) {
238     double precision = getPrecisionStrictBdm(type);
239     double recall = getRecallStrictBdm(type);
240     double betaSq = beta * beta;
241     double answer = ((betaSq + 1* precision * recall)
242                   (betaSq * precision + recall);
243     if(Double.isNaN(answer)) answer = 0.0;
244     return answer;
245   }
246 
247   public double getFMeasureStrictBdm(double beta) {
248     double result = 0;
249     for (String type : differByTypeMap.keySet()) {
250       result += getFMeasureStrictBdm(type, beta);
251     }
252     return result / differByTypeMap.size();
253   }
254 
255   public double getPrecisionLenientBdm(String type) {
256     AnnotationDiffer differ = differByTypeMap.get(type);
257     if (differ.getCorrectMatches() + differ.getSpurious() == 0) {
258       return 1.0;
259     }
260     return (bdmByTypeMap.get(type+ differ.getCorrectMatches()
261           + differ.getPartiallyCorrectMatches())
262          (differ.getCorrectMatches() + differ.getSpurious());
263   }
264 
265   public double getPrecisionLenientBdm() {
266     double result = 0;
267     for (String type : differByTypeMap.keySet()) {
268       result += getPrecisionLenientBdm(type);
269     }
270     return result / differByTypeMap.size();
271   }
272 
273   public double getRecallLenientBdm(String type) {
274     AnnotationDiffer differ = differByTypeMap.get(type);
275     if (differ.getCorrectMatches() + differ.getMissing() == 0) {
276       return 1.0;
277     }
278     return (bdmByTypeMap.get(type+ differ.getCorrectMatches()
279           + differ.getPartiallyCorrectMatches())
280          (differ.getCorrectMatches() + differ.getMissing());
281   }
282 
283   public double getRecallLenientBdm() {
284     double result = 0;
285     for (String type : differByTypeMap.keySet()) {
286       result += getRecallLenientBdm(type);
287     }
288     return result / differByTypeMap.size();
289   }
290 
291   public double getFMeasureLenientBdm(String type, double beta) {
292     double precision = getPrecisionLenientBdm(type);
293     double recall = getRecallLenientBdm(type);
294     double betaSq = beta * beta;
295     double answer = ((betaSq + 1* precision * recall)
296                   (betaSq * precision + recall);
297     if(Double.isNaN(answer)) answer = 0.0;
298     return answer;
299   }
300 
301   public double getFMeasureLenientBdm(double beta) {
302     double result = 0;
303     for (String type : differByTypeMap.keySet()) {
304       result += getFMeasureLenientBdm(type, beta);
305     }
306     return result / differByTypeMap.size();
307   }
308 
309   public double getPrecisionAverageBdm(String type) {
310     return (getPrecisionLenientBdm(type+ getPrecisionStrictBdm(type)) 2.0;
311   }
312 
313   /**
314    * Gets the average of the strict and lenient precision values.
315    @return <tt>double</tt> value.
316    */
317   public double getPrecisionAverageBdm() {
318     return (getPrecisionLenientBdm() + getPrecisionStrictBdm()) 2.0;
319   }
320 
321   public double getRecallAverageBdm(String type) {
322     return (getRecallLenientBdm(type+ getRecallStrictBdm(type)) 2.0;
323   }
324 
325   /**
326    * Gets the average of the strict and lenient recall values.
327    @return <tt>double</tt> value.
328    */
329   public double getRecallAverageBdm() {
330     return (getRecallLenientBdm() + getRecallStrictBdm()) 2.0;
331   }
332 
333   public double getFMeasureAverageBdm(String type, double beta) {
334     return (getFMeasureLenientBdm(type, beta)
335           + getFMeasureStrictBdm(type, beta))
336           2.0;
337   }
338 
339   /**
340    * Gets the average of strict and lenient F-Measure values.
341    @param beta The relative weight of precision and recall. A value of 1
342    * gives equal weights to precision and recall. A value of 0 takes the recall
343    * value completely out of the equation.
344    @return <tt>double</tt>value.
345    */
346   public double getFMeasureAverageBdm(double beta) {
347     return (getFMeasureLenientBdm(beta+ getFMeasureStrictBdm(beta)) 2.0;
348   }
349 
350   public void setBdmFile(URL url) {
351     bdmFileUrl = url;
352     bdmByConceptsMap = null;
353   }
354 
355   /**
356    * Read the BDM scores from a file.
357    @param bdmFile URL of the BDM file
358    @return map from a pair of concepts to their BDM score
359    */
360   public Map<String, Float> read(URL bdmFile) {
361     Map<String, Float> bdmByConceptsMap = new HashMap<String, Float>();
362     if (bdmFile == null) {
363       Out.prln("There is no BDM file specified.");
364       return bdmByConceptsMap;
365     }
366     BufferedReader bdmResultsReader = null;
367     try {
368       bdmResultsReader = new BomStrippingInputStreamReader(
369         new FileInputStream(Files.fileFromURL(bdmFile))"UTF-8");
370       bdmResultsReader.readLine()// skip the first line as the header
371       String line = bdmResultsReader.readLine();
372       while (line != null) {
373         String[] terms = line.split(", ");
374         if (terms.length > 3) {
375           String oneCon = terms[0].substring(4);
376           String anoCon = terms[1].substring(9);
377           String bdmS = terms[2].substring(4);
378           bdmByConceptsMap.put(oneCon + ", " + anoCon, new Float(bdmS));
379         else {
380           Out.prln("File " + bdmFile.toString() " has incorrect format" +
381             "for the line [" + line + "].");
382         }
383         line = bdmResultsReader.readLine();
384       }
385 
386     catch(Exception e) {
387       Out.prln("There is something wrong with the BDM file.");
388       e.printStackTrace();
389 
390     finally {
391       if (bdmResultsReader != null) {
392         try {
393           bdmResultsReader.close();
394         catch (IOException e) {
395           e.printStackTrace();
396         }
397       }
398     }
399     return bdmByConceptsMap;
400   }
401 
402   public List<String> getMeasuresRow(Object[] measures, String title) {
403     List<AnnotationDiffer> differs = new ArrayList<AnnotationDiffer>(
404       getDifferByTypeMap().values());
405     AnnotationDiffer differ = new AnnotationDiffer(differs);
406     NumberFormat f = NumberFormat.getInstance(Locale.ENGLISH);
407     f.setMaximumFractionDigits(2);
408     f.setMinimumFractionDigits(2);
409     List<String> row = new ArrayList<String>();
410     row.add(title);
411     row.add(Integer.toString(differ.getCorrectMatches()));
412     row.add(Integer.toString(differ.getMissing()));
413     row.add(Integer.toString(differ.getSpurious()));
414     row.add(Integer.toString(differ.getPartiallyCorrectMatches()));
415     for (Object object : measures) {
416       String measure = (Stringobject;
417       double beta = Double.valueOf(
418         measure.substring(1,measure.indexOf('-')));
419       if (measure.endsWith("strict")) {
420         row.add(f.format(differ.getPrecisionStrict()));
421         row.add(f.format(differ.getRecallStrict()));
422         row.add(f.format(differ.getFMeasureStrict(beta)));
423       else if (measure.endsWith("strict BDM")) {
424         row.add(f.format(getPrecisionStrictBdm()));
425         row.add(f.format(getRecallStrictBdm()));
426         row.add(f.format(getFMeasureStrictBdm(beta)));
427       else if (measure.endsWith("lenient")) {
428         row.add(f.format(differ.getPrecisionLenient()));
429         row.add(f.format(differ.getRecallLenient()));
430         row.add(f.format(differ.getFMeasureLenient(beta)));
431       else if (measure.endsWith("lenient BDM")) {
432         row.add(f.format(getPrecisionLenientBdm()));
433         row.add(f.format(getRecallLenientBdm()));
434         row.add(f.format(getFMeasureLenientBdm(beta)));
435       else if (measure.endsWith("average")) {
436         row.add(f.format(differ.getPrecisionAverage()));
437         row.add(f.format(differ.getRecallAverage()));
438         row.add(f.format(differ.getFMeasureAverage(beta)));
439       else if (measure.endsWith("average BDM")) {
440         row.add(f.format(getPrecisionAverageBdm()));
441         row.add(f.format(getRecallAverageBdm()));
442         row.add(f.format(getFMeasureAverageBdm(beta)));
443       }
444     }
445     return row;
446   }
447 
448   /**
449    * Be careful, don't modify it.
450    * That's not a copy because it would take too much memory.
451    @return differ by type map
452    */
453   public Map<String, AnnotationDiffer> getDifferByTypeMap() {
454     return differByTypeMap;
455   }
456 
457   protected Map<String, Float> bdmByTypeMap = new HashMap<String, Float>();
458   protected URL bdmFileUrl;
459   protected Map<String, AnnotationDiffer> differByTypeMap =
460     new HashMap<String, AnnotationDiffer>();
461   protected Map<String, Float> bdmByConceptsMap;
462 }