All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.oliviercailloux.grade.format.CsvGrades Maven / Gradle / Ivy

The newest version!
package io.github.oliviercailloux.grade.format;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Verify.verify;

import com.google.common.base.Verify;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Range;
import com.google.common.collect.Streams;
import com.google.common.math.DoubleMath;
import com.google.common.math.Stats;
import com.univocity.parsers.csv.CsvWriter;
import com.univocity.parsers.csv.CsvWriterSettings;
import io.github.oliviercailloux.git.github.model.GitHubUsername;
import io.github.oliviercailloux.grade.Criterion;
import io.github.oliviercailloux.grade.CriterionGradeWeight;
import io.github.oliviercailloux.grade.Grade;
import io.github.oliviercailloux.grade.GradeAggregator;
import io.github.oliviercailloux.grade.IGrade;
import io.github.oliviercailloux.grade.IGrade.CriteriaPath;
import io.github.oliviercailloux.grade.MarksTree;
import io.github.oliviercailloux.grade.WeightingGrade;
import io.github.oliviercailloux.grade.WeightingGradeAggregator;
import io.github.oliviercailloux.grade.comm.StudentOnGitHub;
import io.github.oliviercailloux.grade.comm.StudentOnGitHubKnown;
import java.io.StringWriter;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CsvGrades {
  @SuppressWarnings("unused")
  private static final Logger LOGGER = LoggerFactory.getLogger(CsvGrades.class);

  public static final double DEFAULT_DENOMINATOR = 20d;

  public static final Function> STUDENT_NAME_FUNCTION =
      s -> ImmutableMap.of("Name", s);

  public static final Function> STUDENT_USERNAME_FUNCTION =
          s -> ImmutableMap.of("Username", s.getUsername());

  public static final Function> STUDENT_GENERIC_NAME_FUNCTION =
      s -> ImmutableMap.of("Name", s.toString());

  public static final Function> STUDENT_IDENTITY_FUNCTION = s -> ImmutableMap.of("Name",
          s.hasInstitutionalPart() ? s.toInstitutionalStudent().getLastName() : "unknown",
          "GitHub username", s.getGitHubUsername().getUsername());

  public static final Function> STUDENT_KNOWN_IDENTITY_FUNCTION = s -> ImmutableMap.of("Name",
          s.getLastName(), "GitHub username", s.getGitHubUsername().getUsername());

  public static  CsvGrades
      newInstance(Function> identityFunction, double denominator) {
    return new CsvGrades<>(identityFunction, denominator);
  }

  public static record GenericExam (GradeAggregator aggregator,
      ImmutableMap trees) {
    public GenericExam(GradeAggregator aggregator, Map trees) {
      this(aggregator, ImmutableMap.copyOf(trees));
    }

    public ImmutableSet getUsernames() {
      return trees.keySet();
    }

    public Grade getGrade(K username) {
      return Grade.given(aggregator, trees.get(username));
    }
  }

  public static record PerCriterionWeightingExam (WeightingGradeAggregator aggregator,
      ImmutableMap trees) {
    private static Stream getSuccessors(MarksTree grade, CriteriaPath prefix) {
      return Streams.concat(Stream.of(prefix), grade.getCriteria().stream().map(prefix::withSuffix)
          .flatMap(p -> getSuccessors(grade.getTree(p.getTail()), p)));
    }

    public ImmutableSet getUsernames() {
      return trees.keySet();
    }

    public Grade getGrade(K username) {
      return Grade.given(aggregator, trees.get(username));
    }

    public double weight(CriteriaPath path) {
      return aggregator.weight(path);
    }

    public DoubleStream points(CriteriaPath path) {
      return getUsernames().stream().map(this::getGrade).mapToDouble(
          g -> g.toMarksTree().hasPath(path) ? g.getGrade(path).mark().getPoints() : 0d);
    }

    public double points(K key, CriteriaPath path) {
      final Grade grade = getGrade(key);
      return grade.toMarksTree().hasPath(path) ? grade.mark(path).getPoints() : 0d;
    }

    public double averagePoints(CriteriaPath path) {
      return Stats.of(points(path)).mean();
    }

    public ImmutableSet allPaths() {
      return trees.values().stream().flatMap(g -> getSuccessors(g, CriteriaPath.ROOT))
          .collect(ImmutableSet.toImmutableSet());
    }
  }

  private static  PerCriterionWeightingExam toPerCriterionWeightingExam(GenericExam exam) {
    final PerCriterionWeightingExam newExam = new PerCriterionWeightingExam<>(
        Grade.transformToPerCriterionWeighting(exam.aggregator()), Maps.toMap(exam.getUsernames(),
            u -> Grade.adaptMarksForPerCriterionWeighting(exam.getGrade(u))));

    verify(exam.getUsernames().equals(newExam.getUsernames()));
    verify(exam.getUsernames().stream()
        .allMatch(u -> DoubleMath.fuzzyEquals(exam.getGrade(u).mark().getPoints(),
            newExam.getGrade(u).mark().getPoints(), 1e-6d)));
    return newExam;
  }

  private static String shorten(CriteriaPath gradePath) {
    if (gradePath.isRoot()) {
      return "POINTS";
    }
    return gradePath.toSimpleString();
  }

  private static Stream>
      childrenAsStream(Entry parent) {
    final Stream> itself = Stream.of(parent);
    final IGrade grade = parent.getValue();
    final Stream> allSubGrades =
        grade.getSubGrades().entrySet().stream().flatMap(CsvGrades::childrenAsStream);
    return Stream.concat(itself, allSubGrades);
  }

  @SuppressWarnings("unused")
  private static Stream> childrenAsStream(IGrade parent) {
    if (parent.getSubGrades().isEmpty()) {
      return Stream.of();
    }
    return parent.getSubGrades().entrySet().stream().flatMap(CsvGrades::childrenAsStream);
  }

  private static Stream asContextualizedStream(CriterionGradeWeight parent) {
    final Stream itself = Stream.of(parent);
    final ImmutableSet subGrades;
    if (parent.getGrade() instanceof WeightingGrade) {
      WeightingGrade weightingParent = (WeightingGrade) parent.getGrade();
      subGrades = weightingParent.getSubGradesAsSet();
    } else {
      checkArgument(parent.getGrade().getSubGrades().isEmpty());
      subGrades = ImmutableSet.of();
    }
    final Stream mapped = subGrades.stream()
        .map((cwg) -> CriterionGradeWeight.from(
            Criterion.given(parent.getCriterion().getName() + "/" + cwg.getCriterion().getName()),
            cwg.getGrade(), parent.getWeight() * cwg.getWeight()));
    final Stream flatmappedChildren =
        mapped.flatMap((cwg) -> asContextualizedStream(cwg));
    return Stream.concat(itself, flatmappedChildren);
  }

  private static double getPointsScaled(CriterionGradeWeight cgw, double denominator) {
    final double points = cgw.getGrade().getPoints();
    final double pointsSigned = cgw.getWeight() >= 0d ? points : 1d - points;
    final double pointsScaled = pointsSigned * cgw.getWeight() * denominator;
    return pointsScaled;
  }

  private static String boundsToString(NumberFormat formatter, Range bounds) {
    final double lower = bounds.lowerEndpoint();
    final double upper = bounds.upperEndpoint();
    final String formattedLower = formatter.format(bounds.lowerEndpoint());
    final String formattedUpper = formatter.format(bounds.upperEndpoint());
    return (lower == upper) ? "{" + formattedLower + "}"
        : "[" + formattedLower + ", " + formattedUpper + "]";
  }

  private Function> identityFunction;
  private double denominator;

  private CsvGrades(Function> identityFunction,
      double denominator) {
    this.identityFunction = checkNotNull(identityFunction);
    checkArgument(Double.isFinite(denominator));
    this.denominator = denominator;
  }

  public Function> getIdentityFunction() {
    return identityFunction;
  }

  public CsvGrades
      setIdentityFunction(Function> identityFunction) {
    this.identityFunction = checkNotNull(identityFunction);
    return this;
  }

  public double getDenominator() {
    return denominator;
  }

  public CsvGrades setDenominator(double denominator) {
    this.denominator = denominator;
    return this;
  }

  public String toCsv(Map grades) {
    final Set keys = grades.keySet();
    checkArgument(!keys.isEmpty(), "Can’t determine identity headers with no keys.");

    final NumberFormat formatter = NumberFormat.getNumberInstance(Locale.ENGLISH);
    final StringWriter stringWriter = new StringWriter();
    final CsvWriter writer = new CsvWriter(stringWriter, new CsvWriterSettings());

    final ImmutableMap weightingGrades =
        grades.entrySet().stream().filter(e -> e.getValue() instanceof WeightingGrade).collect(
            ImmutableMap.toImmutableMap(Map.Entry::getKey, e -> (WeightingGrade) e.getValue()));
    final ImmutableSetMultimap perKey = weightingGrades.entrySet().stream().collect(
            ImmutableSetMultimap.flatteningToImmutableSetMultimap(Entry::getKey, (e) -> e.getValue()
                .getSubGradesAsSet().stream().flatMap(CsvGrades::asContextualizedStream)));
    final ImmutableTable asTable =
        perKey.entries().stream().collect(ImmutableTable.toImmutableTable(Entry::getKey,
            (e) -> e.getValue().getCriterion(), Entry::getValue));
    LOGGER.debug("From {}, obtained {}, as table {}.", grades, perKey, asTable);

    final ImmutableSet allCriteria = asTable.columnKeySet();

    final ImmutableSet identityHeadersFromFunction =
        keys.stream().flatMap(k -> identityFunction.apply(k).keySet().stream()).distinct()
            .collect(ImmutableSet.toImmutableSet());
    final ImmutableSet identityHeaders =
        identityHeadersFromFunction.isEmpty() ? ImmutableSet.of("") : identityHeadersFromFunction;
    final ImmutableList headers =
        Streams.concat(identityHeaders.stream(), allCriteria.stream().map(Criterion::getName),
            Stream.of("Points")).collect(ImmutableList.toImmutableList());
    writer.writeHeaders(headers);

    final String firstHeader = identityHeaders.iterator().next();

    for (K key : keys) {
      final Map identity = identityFunction.apply(key);
      identity.entrySet().forEach(e -> writer.addValue(e.getKey(), e.getValue()));

      final IGrade grade = grades.get(key);
      final ImmutableCollection marks = asTable.row(key).values();
      LOGGER.debug("Writing {} and {}.", key, marks);
      for (CriterionGradeWeight cgw : marks) {
        final Criterion criterion = cgw.getCriterion();
        Verify.verify(allCriteria.contains(criterion));
        final double pointsScaled = getPointsScaled(cgw, denominator);
        writer.addValue(criterion.getName(), formatter.format(pointsScaled));
      }

      writer.addValue("Points", formatter.format(grade.getPoints() * denominator));
      writer.writeValuesToRow();
    }
    writer.writeEmptyRow();

    final ImmutableMap> weightsPerCriterion = asTable.columnKeySet().stream()
            .collect(ImmutableMap.toImmutableMap(c -> c, c -> asTable.column(c).values().stream()
                .map(CriterionGradeWeight::getWeight).collect(ImmutableSet.toImmutableSet())));
    final boolean homogeneousWeights =
        weightsPerCriterion.values().stream().allMatch(s -> s.size() == 1);

    LOGGER.debug("Writing summary data.");
    {
      writer.addValue(firstHeader, "Range");
      if (homogeneousWeights) {
        for (Criterion criterion : allCriteria) {
          final double weight = Iterables.getOnlyElement(weightsPerCriterion.get(criterion));
          final Range bounds = Range.closed(0d, weight * denominator);
          writer.addValue(criterion.getName(), boundsToString(formatter, bounds));
        }
      }
      final Range overallBounds = Range.closed(0d, denominator);
      writer.addValue("Points", boundsToString(formatter, overallBounds));
      writer.writeValuesToRow();
    }

    {
      writer.addValue(firstHeader, "Upper bound");
      if (homogeneousWeights) {
        for (Criterion criterion : allCriteria) {
          final double weight = Iterables.getOnlyElement(weightsPerCriterion.get(criterion));
          writer.addValue(criterion.getName(), formatter.format(weight * denominator));
        }
      }
      writer.addValue("Points", formatter.format(denominator));
      writer.writeValuesToRow();
    }

    {
      writer.addValue(firstHeader, "Average");
      for (Criterion criterion : allCriteria) {
        final double average;
        if (homogeneousWeights) {
          average = asTable.column(criterion).values().stream()
              .collect(Collectors.averagingDouble(c -> getPointsScaled(c, denominator)));
        } else {
          average = asTable.column(criterion).values().stream()
              .collect(Collectors.averagingDouble(c -> c.getGrade().getPoints()));
        }
        writer.addValue(criterion.getName(), formatter.format(average));
      }
      final double averageOfTotalScore = weightingGrades.values().stream()
          .collect(Collectors.averagingDouble(g -> g.getPoints() * denominator));
      writer.addValue("Points", formatter.format(averageOfTotalScore));
      writer.writeValuesToRow();
    }

    {
      writer.addValue(firstHeader, "Nb > 0");
      for (Criterion criterion : allCriteria) {
        final int nb = Math.toIntExact(asTable.column(criterion).values().stream()
            .filter(c -> c.getGrade().getPoints() > 0d).count());
        writer.addValue(criterion.getName(), formatter.format(nb));
      }
      final int nb = Math
          .toIntExact(weightingGrades.values().stream().filter(g -> g.getPoints() > 0d).count());
      writer.addValue("Points", formatter.format(nb));
      writer.writeValuesToRow();
    }

    {
      writer.addValue(firstHeader, "Nb MAX");
      for (Criterion criterion : allCriteria) {
        final int nb = Math.toIntExact(asTable.column(criterion).values().stream()
            .filter(c -> c.getGrade().getPoints() == 1d).count());
        writer.addValue(criterion.getName(), formatter.format(nb));
      }
      final int nb = Math
          .toIntExact(weightingGrades.values().stream().filter(g -> g.getPoints() == 1d).count());
      writer.addValue("Points", formatter.format(nb));
      writer.writeValuesToRow();
    }

    LOGGER.debug("Done writing.");
    writer.close();

    return stringWriter.toString();
  }

  public  String gradesToCsv(GradeAggregator aggregator,
      Map trees) {
    final Set keys = trees.keySet();
    checkArgument(!keys.isEmpty(), "Can’t determine identity headers with no keys.");

    final GenericExam inputExam = new GenericExam<>(aggregator, trees);
    final PerCriterionWeightingExam exam = toPerCriterionWeightingExam(inputExam);

    final NumberFormat formatter = NumberFormat.getNumberInstance(Locale.ENGLISH);
    final StringWriter stringWriter = new StringWriter();
    final CsvWriter writer = new CsvWriter(stringWriter, new CsvWriterSettings());

    final ImmutableSet identityHeadersFromFunction =
        keys.stream().flatMap(k -> identityFunction.apply(k).keySet().stream())
            .collect(ImmutableSet.toImmutableSet());
    final ImmutableSet identityHeaders =
        identityHeadersFromFunction.isEmpty() ? ImmutableSet.of("") : identityHeadersFromFunction;
    final ImmutableSet allPaths = exam.allPaths();

    final ImmutableList headers =
        Streams.concat(identityHeaders.stream(), allPaths.stream().map(CsvGrades::shorten))
            .collect(ImmutableList.toImmutableList());
    writer.writeHeaders(headers);

    final String firstHeader = headers.iterator().next();

    for (L key : keys) {
      final Map identity = identityFunction.apply(key);
      identity.entrySet().forEach(e -> writer.addValue(e.getKey(), e.getValue()));

      allPaths.stream().forEach(p -> writer.addValue(CsvGrades.shorten(p),
          formatter.format(exam.weight(p) * exam.points(key, p) * denominator)));
      writer.writeValuesToRow();
    }
    writer.writeEmptyRow();

    LOGGER.debug("Writing summary data.");
    {
      writer.addValue(firstHeader, "Upper bound");
      allPaths.stream().forEach(p -> writer.addValue(CsvGrades.shorten(p),
          formatter.format(exam.weight(p) * denominator)));
      writer.writeValuesToRow();
    }

    {
      writer.addValue(firstHeader, "Average");
      allPaths.stream().forEach(p -> writer.addValue(CsvGrades.shorten(p),
          formatter.format(exam.weight(p) * exam.averagePoints(p) * denominator)));
      writer.writeValuesToRow();
    }

    {
      writer.addValue(firstHeader, "Nb ≠ 0");
      allPaths.stream().forEach(p -> writer.addValue(CsvGrades.shorten(p),
          formatter.format(exam.points(p).filter(v -> v != 0d).count())));
      writer.writeValuesToRow();
    }

    {
      writer.addValue(firstHeader, "Nb MAX");
      allPaths.stream().forEach(p -> writer.addValue(CsvGrades.shorten(p),
          formatter.format(exam.points(p).filter(v -> v > 1d - 1e-6d).count())));
      writer.writeValuesToRow();
    }

    LOGGER.debug("Done writing.");
    writer.close();

    return stringWriter.toString();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy