Advertisement
ridleygarnier

Untitled

Feb 19th, 2020
251
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 18.61 KB | None | 0 0
  1. """CSC148 Assignment 1
  2.  
  3. === CSC148 Winter 2020 ===
  4. Department of Computer Science,
  5. University of Toronto
  6.  
  7. This code is provided solely for the personal and private use of
  8. students taking the CSC148 course at the University of Toronto.
  9. Copying for purposes other than this use is expressly prohibited.
  10. All forms of distribution of this code, whether as given or with
  11. any changes, are expressly prohibited.
  12.  
  13. Authors: Misha Schwartz, Mario Badr, Christine Murad, Diane Horton, Sophia Huynh
  14. and Jaisie Sin
  15.  
  16. All of the files in this directory and all subdirectories are:
  17. Copyright (c) 2020 Misha Schwartz, Mario Badr, Christine Murad, Diane Horton,
  18. Sophia Huynh and Jaisie Sin
  19.  
  20. === Module Description ===
  21.  
  22. This file contains classes that describe a survey as well as classes that
  23. described different types of questions that can be asked in a given survey.
  24. """
  25. from __future__ import annotations
  26. from typing import TYPE_CHECKING, Union, Dict, List
  27. from criterion import HomogeneousCriterion, InvalidAnswerError
  28. if TYPE_CHECKING:
  29.     from criterion import Criterion
  30.     from grouper import Grouping
  31.     from course import Student
  32.  
  33.  
  34. class Question:
  35.     """ An abstract class representing a question used in a survey
  36.  
  37.    === Public Attributes ===
  38.    id: the id of this question
  39.    text: the text of this question
  40.  
  41.    === Representation Invariants ===
  42.    text is not the empty string
  43.    """
  44.  
  45.     id: int
  46.     text: str
  47.  
  48.     def __init__(self, id_: int, text: str) -> None:
  49.         """ Initialize a question with the text <text> """
  50.         self.id = id_
  51.         self.text = text
  52.  
  53.     def __str__(self) -> str:
  54.         """
  55.        Return a string representation of this question that contains both
  56.        the text of this question and a description of all possible answers
  57.        to this question.
  58.  
  59.        You can choose the precise format of this string.
  60.        """
  61.         raise NotImplementedError
  62.  
  63.     def validate_answer(self, answer: Answer) -> bool:
  64.         """
  65.        Return True iff <answer> is a valid answer to this question.
  66.        """
  67.         raise NotImplementedError
  68.  
  69.     def get_similarity(self, answer1: Answer, answer2: Answer) -> float:
  70.         """ Return a float between 0.0 and 1.0 indicating how similar two
  71.        answers are.
  72.  
  73.        === Precondition ===
  74.        <answer1> and <answer2> are both valid answers to this question
  75.        """
  76.         raise NotImplementedError
  77.  
  78.  
  79. class MultipleChoiceQuestion(Question):
  80.     # TODO: make this a child class of another class defined in this file
  81.     """ A question whose answers can be one of several options
  82.  
  83.    === Public Attributes ===
  84.    id: the id of this question
  85.    text: the text of this question
  86.  
  87.    === Representation Invariants ===
  88.    text is not the empty string
  89.    """
  90.  
  91.     id: int
  92.     text: str
  93.     options: List[str]
  94.  
  95.     def __init__(self, id_: int, text: str, options: List[str]) -> None:
  96.         """
  97.        Initialize a question with the text <text> and id <id> and
  98.        possible answers <options>.
  99.  
  100.        === Precondition ===
  101.        No two elements in <options> are the same string
  102.        <options> contains at least two elements
  103.        """
  104.         Question.__init__(self, id_, text)
  105.         self.options = options
  106.  
  107.     def __str__(self) -> str:
  108.         """
  109.        Return a string representation of this question including the
  110.        text of the question and a description of the possible answers.
  111.  
  112.        You can choose the precise format of this string.
  113.        """
  114.         return '''Question:{0}. Possible Answers:{1}'''.format(self.text, \
  115.                                                                self.options)
  116.  
  117.     def validate_answer(self, answer: Answer) -> bool:
  118.         """
  119.        Return True iff <answer> is a valid answer to this question.
  120.  
  121.        An answer is valid if its content is one of the possible answers to this
  122.        question.
  123.        """
  124.         return answer.content in self.options
  125.  
  126.     def get_similarity(self, answer1: Answer, answer2: Answer) -> float:
  127.         """
  128.        Return 1.0 iff <answer1>.content and <answer2>.content are equal and
  129.        0.0 otherwise.
  130.  
  131.        === Precondition ===
  132.        <answer1> and <answer2> are both valid answers to this question.
  133.        """
  134.         if answer1.content == answer2.content:
  135.             return 1.0
  136.         else:
  137.             return 0.0
  138.  
  139.  
  140. class NumericQuestion(Question):
  141.     """ A question whose answer can be an integer between some
  142.    minimum and maximum value (inclusive).
  143.  
  144.    === Public Attributes ===
  145.    id: the id of this question
  146.    text: the text of this question
  147.  
  148.    === Representation Invariants ===
  149.    text is not the empty string
  150.    """
  151.  
  152.     id: int
  153.     text: str
  154.     min_: int
  155.     max_: int
  156.  
  157.     def __init__(self, id_: int, text: str, min_: int, max_: int) -> None:
  158.         """
  159.        Initialize a question with id <id_> and text <text> whose possible
  160.        answers can be any integer between <min_> and <max_> (inclusive)
  161.  
  162.        === Precondition ===
  163.        min_ < max_
  164.        """
  165.         Question.__init__(self, id_, text)
  166.         self.min_ = min_
  167.         self.max_ = max_
  168.  
  169.     def __str__(self) -> str:
  170.         """
  171.        Return a string representation of this question including the
  172.        text of the question and a description of the possible answers.
  173.  
  174.        You can choose the precise format of this string.
  175.        """
  176.         return 'Question:{}.\
  177.         Possible answers are between {} and {} (inclusive)'.format(self.text, \
  178.                                                                     self.min_, \
  179.                                                                     self.max_)
  180.  
  181.     def validate_answer(self, answer: Answer) -> bool:
  182.         """
  183.        Return True iff the content of <answer> is an integer between the
  184.        minimum and maximum (inclusive) possible answers to this question.
  185.        """
  186.         if isinstance(answer.content, int):
  187.             return self.min_ <= answer.content <= self.max_
  188.         return False
  189.  
  190.     def get_similarity(self, answer1: Answer, answer2: Answer) -> float:
  191.         """
  192.        Return the similarity between <answer1> and <answer2> over the range
  193.        of possible answers to this question.
  194.  
  195.        Similarity calculated by:
  196.  
  197.        1. first find the absolute difference between <answer1>.content and
  198.           <answer2>.content.
  199.        2. divide the value from step 1 by the difference between the maximimum
  200.           and minimum possible answers.
  201.        3. subtract the value from step 2 from 1.0
  202.  
  203.        Hint: this is the same calculation from the worksheet in lecture!
  204.  
  205.        For example:
  206.        - Maximum similarity is 1.0 and occurs when <answer1> == <answer2>
  207.        - Minimum similarity is 0.0 and occurs when <answer1> is the minimum
  208.            possible answer and <answer2> is the maximum possible answer
  209.            (or vice versa).
  210.  
  211.        === Precondition ===
  212.        <answer1> and <answer2> are both valid answers to this question
  213.        """
  214.         a = abs(answer1.content - answer2.content)
  215.         b = a / (self.max_ - self.min_)
  216.         return 1.0 - b
  217.  
  218.  
  219. class YesNoQuestion (MultipleChoiceQuestion):
  220.     """ A question whose answer is either yes (represented by True) or
  221.    no (represented by False).
  222.  
  223.    === Public Attributes ===
  224.    id: the id of this question
  225.    text: the text of this question
  226.  
  227.    === Representation Invariants ===
  228.    text is not the empty string
  229.    """
  230.     id: int
  231.     text: str
  232.  
  233.     def __init__(self, id_: int, text: str) -> None:
  234.         """
  235.        Initialize a question with the text <text> and id <id>.
  236.        """
  237.         self.id = id_
  238.         self.text = text
  239.  
  240.     def __str__(self) -> str:
  241.         """
  242.        Return a string representation of this question including the
  243.        text of the question and a description of the possible answers.
  244.  
  245.        You can choose the precise format of this string.
  246.        """
  247.         return "Question:{}. Possible answers are True or False." \
  248.             .format(self.text)
  249.  
  250.     def validate_answer(self, answer: Answer) -> bool:
  251.         """
  252.        Return True iff <answer>'s content is a boolean.
  253.        """
  254.         return isinstance(answer.content, bool)
  255.  
  256.  
  257. class CheckboxQuestion(MultipleChoiceQuestion):
  258.     """ A question whose answers can be one or more of several options
  259.  
  260.    === Public Attributes ===
  261.    id: the id of this question
  262.    text: the text of this question
  263.  
  264.    === Representation Invariants ===
  265.    text is not the empty string
  266.    """
  267.  
  268.     id: int
  269.     text: str
  270.     options: List[str]
  271.  
  272.     def __init__(self, id_: int, text: str, options: List[str]) -> None:
  273.         """
  274.        Initialize a question with the text <text> and id <id> and
  275.        possible answers <options>.
  276.  
  277.        === Precondition ===
  278.        No two elements in <options> are the same string
  279.        <options> contains at least two elements
  280.        """
  281.  
  282.         MultipleChoiceQuestion.__init__(self, id_, text, options)
  283.  
  284.     def validate_answer(self, answer: Answer) -> bool:
  285.         """
  286.        Return True iff <answer> is a valid answer to this question.
  287.  
  288.        An answer is valid iff its content is a non-empty list containing
  289.        unique possible answers to this question.
  290.        """
  291.         if isinstance(answer.content, list) and answer.content != []:
  292.             answer_lst = []
  293.             for a in answer.content:
  294.                 if a not in answer_lst:
  295.                     answer_lst.append(a)
  296.                 else:
  297.                     return False
  298.             return True
  299.         return False
  300.  
  301.     def get_similarity(self, answer1: Answer, answer2: Answer) -> float:
  302.         """
  303.        Return the similarity between <answer1> and <answer2>.
  304.  
  305.        Similarity is defined as the ratio between the number of strings that
  306.        are common to both <answer1>.content and <answer2>.content over the
  307.        total number of unique strings that appear in both <answer1>.content and
  308.        <answer2>.content
  309.  
  310.        For example, if <answer1>.content == ['a', 'b', 'c'] and
  311.        <answer2>.content == ['c', 'b', 'd'], the strings that are common to
  312.        both are ['c', 'b'] and the unique strings that appear in both are
  313.        ['a', 'b', 'c', 'd'].
  314.  
  315.        === Precondition ===
  316.        <answer1> and <answer2> are both valid answers to this question
  317.        """
  318.         lst1 = [value for value in answer1.content if value in answer2.content]
  319.         lst2 = answer1.content + \
  320.                list(set(answer2.content) - set(answer1.content))
  321.         return len(lst1) / len(lst2)
  322.  
  323.  
  324. class Answer:
  325.     """ An answer to a question used in a survey
  326.  
  327.    === Public Attributes ===
  328.    content: an answer to a single question
  329.    """
  330.     content: Union[str, bool, int, List[str]]
  331.  
  332.     def __init__(self,
  333.                  content: Union[str, bool, int, List[Union[str]]]) -> None:
  334.         """Initialize an answer with content <content>"""
  335.         self.content = content
  336.  
  337.     def is_valid(self, question: Question) -> bool:
  338.         """Return True iff self.content is a valid answer to <question>"""
  339.         return question.validate_answer(self)
  340.  
  341.  
  342. class Survey:
  343.     """
  344.    A survey containing questions as well as criteria and weights used to
  345.    evaluate the quality of a group based on their answers to the survey
  346.    questions.
  347.  
  348.    === Private Attributes ===
  349.    _questions: a dictionary mapping each question's id to the question itself
  350.    _criteria: a dictionary mapping a question's id to its associated criterion
  351.    _weights: a dictionary mapping a question's id to a weight; an integer
  352.              representing the importance of this criteria.
  353.    _default_criterion: a criterion to use to evaluate a question if the
  354.              question does not have an associated criterion in _criteria
  355.    _default_weight: a weight to use to evaluate a question if the
  356.              question does not have an associated weight in _weights
  357.  
  358.    === Representation Invariants ===
  359.    No two questions on this survey have the same id
  360.    Each key in _questions equals the id attribute of its value
  361.    Each key in _criteria occurs as a key in _questions
  362.    Each key in _weights occurs as a key in _questions
  363.    Each value in _weights is greater than 0
  364.    _default_weight > 0
  365.    """
  366.  
  367.     _questions: Dict[int, Question]
  368.     _criteria: Dict[int, Criterion]
  369.     _weights: Dict[int, int]
  370.     _default_criterion: Criterion
  371.     _default_weight: int
  372.  
  373.     def __init__(self, questions: List[Question]) -> None:
  374.         """
  375.        Initialize a new survey that contains every question in <questions>.
  376.        This new survey should use a HomogeneousCriterion as a default criterion
  377.        and should use 1 as a default weight.
  378.        """
  379.         self._questions = {}
  380.         self._criteria = {}
  381.         self._weights = {}
  382.         self._default_criterion = HomogeneousCriterion()
  383.         self._default_weight = 1
  384.  
  385.         for question in questions:
  386.             q = question.id
  387.             self._questions[q] = question
  388.  
  389.     def __len__(self) -> int:
  390.         """ Return the number of questions in this survey """
  391.         return len(self._questions)
  392.  
  393.     def __contains__(self, question: Question) -> bool:
  394.         """
  395.        Return True iff there is a question in this survey with the same
  396.        id as <question>.
  397.        """
  398.         return question.id in self._questions.keys()
  399.  
  400.     def __str__(self) -> str:
  401.         """
  402.        Return a string containing the string representation of all
  403.        questions in this survey
  404.  
  405.        You can choose the precise format of this string.
  406.        """
  407.         formatted_string = ''
  408.         i = 0
  409.  
  410.         for key in self._questions:
  411.             i += 1
  412.             formatted_string += 'Question{}:{}.'.format(i, \
  413.                                                         self._questions[key])
  414.         return formatted_string
  415.  
  416.     def get_questions(self) -> List[Question]:
  417.         """ Return a list of all questions in this survey """
  418.         return list(self._questions.values())
  419.  
  420.     def _get_criterion(self, question: Question) -> Criterion:
  421.         """
  422.        Return the criterion associated with <question> in this survey.
  423.  
  424.        Iff <question>.id does not appear in self._criteria, return the default
  425.        criterion for this survey instead.
  426.  
  427.        === Precondition ===
  428.        <question>.id occurs in this survey
  429.        """
  430.         if question.id in self._criteria:
  431.             return self._criteria[question.id]
  432.         return self._default_criterion
  433.  
  434.     def _get_weight(self, question: Question) -> int:
  435.         """
  436.        Return the weight associated with <question> in this survey.
  437.  
  438.        Iff <question>.id does not appear in self._weights, return the default
  439.        weight for this survey instead.
  440.  
  441.        === Precondition ===
  442.        <question>.id occurs in this survey
  443.        """
  444.         if question.id in self._weights:
  445.             return self._weights[question.id]
  446.         return self._default_weight
  447.  
  448.     def set_weight(self, weight: int, question: Question) -> bool:
  449.         """
  450.        Set the weight associated with <question> to <weight> and return True.
  451.  
  452.        If <question>.id does not occur in this survey, do not set the <weight>
  453.        and return False instead.
  454.        """
  455.         if question.id in self._questions:
  456.             self._weights[question.id] = weight
  457.             return True
  458.         return False
  459.  
  460.     def set_criterion(self, criterion: Criterion, question: Question) -> bool:
  461.         """
  462.        Set the criterion associated with <question> to <criterion> and return
  463.        True.
  464.  
  465.        If <question>.id does not occur in this survey, do not set the\
  466.         <criterion> and return False instead.
  467.        """
  468.         if question.id in self._questions:
  469.             self._criteria[question.id] = criterion
  470.             return True
  471.         return False
  472.  
  473.     def score_students(self, students: List[Student]) -> float:
  474.         """
  475.        Return a quality score for <students> calculated based on their answers
  476.        to the questions in this survey, and the associated criterion and weight
  477.        for each question .
  478.  
  479.        This score is determined using the following algorithm:
  480.  
  481.        1. For each question in <self>, find its associated criterion, weight,
  482.           and <students> answers to this question. Use the score_answers method
  483.           for this criterion to calculate a quality score. Multiply this
  484.           quality score by the associated weight.
  485.        2. Find the average of all quality scores from step 1.
  486.  
  487.        If an InvalidAnswerError would be raised by calling this method, or if
  488.        there are no questions in <self>, this method should return zero.
  489.  
  490.        === Precondition ===
  491.        All students in <students> have an answer to all questions in this
  492.            survey
  493.        """
  494.         scores = []
  495.         if not self._questions:
  496.             return 0.0
  497.         else:
  498.             for question in self._questions:
  499.                 student_answers = []
  500.                 for student in students:
  501.                     student_answers.append(student.get_answer(self._questions[question]))
  502.                 x = self._get_criterion(self._questions[question])
  503.                 scores.append(x.score_answers(self._questions[question],
  504.                                               student_answers) * self._get_weight(self._questions[question]))
  505.  
  506.             if not (InvalidAnswerError in scores):
  507.                 return sum(scores) / len(scores)
  508.             else:
  509.                 return 0.0
  510.  
  511.     def score_grouping(self, grouping: Grouping) -> float:
  512.         """ Return a score for <grouping> calculated based on the answers of
  513.        each student in each group in <grouping> to the questions in <self>.
  514.  
  515.        If there are no groups in <grouping> this score is 0.0. Otherwise, this
  516.        score is determined using the following algorithm:
  517.  
  518.        1. For each group in <grouping>, get the score for the members of this
  519.           group calculated based on their answers to the questions in this
  520.           survey.
  521.        2. Return the average of all the scores calculated in step 1.
  522.  
  523.        === Precondition ===
  524.        All students in the groups in <grouping> have an answer to all questions
  525.            in this survey
  526.        """
  527.         # TODO: complete the body of this method
  528.  
  529.  
  530. if __name__ == '__main__':
  531.     import python_ta
  532.     python_ta.check_all(config={'extra-imports': ['typing',
  533.                                                   'criterion',
  534.                                                   'course',
  535.                                                   'grouper']})
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement