content: # -*- coding: utf-8 -*-
  
  """
  Evaluation Models
  =================
  
  """
  
  from __future__ import division
  
  from copy import copy
  from itertools import izip
  from collections import defaultdict
  
  import numpy as np
  import pandas as pd
  
  import tools
  
  
  __all__ = (
      'DummyPriorModel',
      'EloModel',
      'EloResponseTime',
      'PFAModel',
      'PFAResponseTime',
      'PFAExt',
      'PFAExtTiming',
      'PFAExtStaircase',
      'PFAExtSpacing',
      'PFAGong',
      'PFAGongTiming',
      'PFATiming',
  )
  
  
  #: Dictionary of the most commonly used time effect functions in this thesis.
  time_effect_funcs = {}
  
  
  def register_time_effect(name):
      """Registers new time effect functions."""
      def register(time_effect):
          time_effect_funcs[name] = time_effect
      return register
  
  
  @register_time_effect('log')
  def time_effect_log(t, a=1.8, c=0.123):
      return a - c * np.log(t)
  
  
  @register_time_effect('pow')
  def time_effect_div(t, a=2, c=0.2):
      return a / (t+1) ** c
  
  
  @register_time_effect('exp')
  def time_effect_exp(t, a=1.6, c=0.01):
      return a * np.exp(-c * np.sqrt(t))
  
  
  def init_time_effect(obj, name, parameters=('a', 'c')):
      """Prepares time effect function based on name. Initializes
      the given object with default parameters `a` and `c`.
  
      :param obj: Object to initialize with time effect function.
      :param name: Name of the time effect function.
      """
      time_effect_fun = time_effect_funcs[name]
      defaults = time_effect_fun.func_defaults
  
      a, c = parameters
  
      if getattr(obj, a, None) is None:
          setattr(obj, a, defaults[0])
      if getattr(obj, c, None) is None:
          setattr(obj, c, defaults[1])
  
      def time_effect(t):
          a_val, c_val = getattr(obj, a), getattr(obj, c)
          return time_effect_fun(t, a_val, c_val)
  
      return time_effect
  
  
  class Question(object):
      """Representation of a question."""
  
      def __init__(self, **kwargs):
          self.id = kwargs.pop('id')
          self.user_id = kwargs.pop('user_id')
          self.place_id = kwargs.pop('place_id')
          self.type = kwargs.pop('type')
          self.inserted = kwargs.pop('inserted')
          self.options = kwargs.pop('options')
  
  
  class Answer(Question):
      """Answer to a question."""
  
      def __init__(self, **kwargs):
          super(Answer, self).__init__(**kwargs)
  
          self.place_answered = kwargs.pop('place_answered')
          self.response_time = kwargs.pop('response_time')
          self.is_correct = kwargs.pop('is_correct')
  
  
  class User(object):
      """Returns a user with given ID.
  
      :param user_id: ID of the user.
      :type user_id: int
      """
      def __init__(self, user_id):
          self.id = user_id
          self.skill_increments = []
  
      @property
      def skill(self):
          """Skill of the user."""
          return sum(self.skill_increments)
  
      @property
      def answers_count(self):
          """Number of answer of the user (equal to the number of
          skill increments.
          """
          return len(self.skill_increments)
  
      def inc_skill(self, increment):
          """Increments the skill of the user.
  
          :param increment: Increment (or decrement) of the skill.
          :type increment: int
          """
          self.skill_increments += [increment]
  
  
  class Place(object):
      """Returns a place with given ID.
  
      :param place_id: ID of the place.
      :type place_id: int
      """
      def __init__(self, place_id):
          self.id = place_id
          self.difficulty_increments = []
  
      @property
      def difficulty(self):
          """Difficulty of the place."""
          return sum(self.difficulty_increments)
  
      @property
      def answers_count(self):
          """Number of answer for the place (equal to the number of
          difficulty increments.
          """
          return len(self.difficulty_increments)
  
      def inc_difficulty(self, increment):
          """Increments the difficulty of the place.
  
          :param increment: Increment (or decrement) of the difficulty.
          :type increment: int
          """
          self.difficulty_increments += [increment]
  
  
  class Item(object):
      """Item representation.
  
      :param prior: Prior skills of users and difficulties of places.
      :type prior: dictionary
      :param user_id: ID of the user.
      :type user_id: int
      :param place_id: ID of the place.
      :type place_id: int
      """
  
      def __init__(self, prior, user_id, place_id):
          self.prior = prior
          self.user_id = user_id
          self.place_id = place_id
  
          self.practices = []
          self.knowledge_increments = []
  
      @property
      def user(self):
          """User answering the item."""
          return self.prior.users[self.user_id]
  
      @property
      def place(self):
          """Place of the item being asked."""
          return self.prior.places[self.place_id]
  
      @property
      def knowledge(self):
          """Knowledge of the item by the user."""
          return (
              (self.user.skill - self.place.difficulty)
              + sum(self.knowledge_increments)
          )
  
      @property
      def correct(self):
          """List of correct answers."""
          return [ans for ans in self.practices if ans.is_correct]
  
      @property
      def incorrect(self):
          """List of incorrect answers."""
          return [ans for ans in self.practices if not ans.is_correct]
  
      @property
      def last_inserted(self):
          """Returns the time of the last answer for this item
          or :obj:`None` if the item was never answered before.
          """
          if self.practices:
              return self.practices[-1].inserted
  
      @property
      def any_incorrect(self):
          """:obj:`True` if at least one of the practiced item
          was answered incorrectly, otherwise :obj:`False`.
          """
          return any(not answer.is_correct for answer in self.practices)
  
      def get_diffs(self, current):
          """Returns list of previous practices expresed as the number
          of seconds that passed between *current* practice and all
          the *previous* practices.
  
          :param current: Datetime of the current practice.
          :type place: string
          """
          return [
              tools.time_diff(current, prior.inserted)
              for prior in self.practices
          ]
  
      def inc_knowledge(self, increment):
          """Increments the knowledge of the user of the item.
  
          :param increment: Increment (or decrement) of the knowledge.
          :type increment: int
          """
          self.knowledge_increments += [increment]
  
      def add_practice(self, answer):
          """Registers new practice of the item.
  
          :param answer: Information about the answer.
          :type answer: :class:`pandas.Series` or :class:`Answer`
          """
          if isinstance(answer, pd.Series):
              self.practices += [Answer(**answer.to_dict())]
          else:
              self.practices += [copy(answer)]
  
  
  class Model(object):
      """Abstract model class."""
  
      ABBR = None
  
      def respect_guess(self, prediction, options):
          """Updates prediction with respect to guessing paramter.
  
          :param prediction: Prediction calculated so far.
          :type prediction: float
          :param options: Number of options in the multiple-choice question.
          :type options: int
          """
          if options:
              val = 1 / len(options)
              return val + (1 - val) * prediction
          else:
              return prediction
  
      def predict(self, question):
          """Returns probability of correct answer for given question.
  
          :param question: Asked question.
          :type question: :class:`pandas.Series` or :class:`Question`
          """
          raise NotImplementedError()
  
      def update(self, answer):
          """Performes an update of skills, difficulties or knowledge.
  
          :param answer: Asked question.
          :type answer: :class:`pandas.Series` or :class:`Answer`
          """
          raise NotImplementedError()
  
      def train(self, data):
          """Trains the model on given data set.
  
          :param data: Data set on which to train the model.
          :type data: :class:`pandas.DataFrame`
          """
          raise NotImplementedError()
  
      @classmethod
      def split_data(cls, data, ratio=0.7):
          """Classmethod that splits data into training set and test set.
  
          :param data: The object containing data.
          :type data: :class:`pandas.DataFrame`.
          :param ratio: What portion of data to include in the training set
              and the test set. :obj:`0.5` means that the data will be
              distributed equaly.
          :type ratio: float
          """
          raise NotImplementedError()
  
  
  class DummyPriorModel(Model):
      """Dummy model that sets all skills of users and difficulties
      of places to zero.
      """
  
      class _User(object):
          """Returns a user with given ID."""
  
          def __init__(self, skill):
              self.skill = skill
  
      class _Place(object):
          """Returns a place with given ID."""
  
          def __init__(self, difficulty):
              self.difficulty = difficulty
  
      def __init__(self, skill=0.0, difficulty=0.0):
          self.users = defaultdict(lambda: self._User(skill))
          self.places = defaultdict(lambda: self._Place(difficulty))
  
      def update(self, answer):
          pass
  
      def train(self, data):
          pass
  
  
  class EloModel(Model):
      """Predicts correctness of answers using Elo Rating System.
      The model is parametrized with `alpha` and `beta`. These parameters
      affect the uncertainty function.
      """
      ABBR = 'Elo'
  
      def __init__(self, alpha=1, beta=0.05):
          self.alpha = alpha
          self.beta = beta
  
          self.init_model()
  
      def init_model(self):
          """Initializes two attributes of the model. Both attributes are
          dataframes. The first attribute represents difficulties of countries.
          The second attribute represents global knowledge of students.
          """
          self.places = tools.keydefaultdict(Place)
          self.users = tools.keydefaultdict(User)
  
          self.predictions = {}
  
      def uncertainty(self, n):
          """Uncertainty function. The purpose is to make each update on
          the model trained with sequence of `n` answers less and less
          significant as the number of prior answers is bigger.
  
          :param n: Number of user's answers or total answers to a place.
          :type n: int
          """
          return self.alpha / (1 + self.beta * n)
  
      def predict(self, question):
          """Returns probability of correct answer for given question.
  
          :param question: Asked question.
          :type question: :class:`pandas.Series` or :class:`Question`
          """
          user = self.users[question.user_id]
          place = self.places[question.place_id]
  
          prediction = tools.sigmoid(user.skill - place.difficulty)
          return self.respect_guess(prediction, question.options)
  
      def update(self, answer):
          """Updates skills of users and difficulties of places according
          to given answer.
  
          :param answer: Answer to a question.
          :type answer: :class:`pandas.Series`
          """
          user = self.users[answer.user_id]
          place = self.places[answer.place_id]
  
          prediction = self.predict(answer)
          shift = answer.is_correct - prediction
  
          user.inc_skill(self.uncertainty(user.answers_count) * shift)
          place.inc_difficulty(-(self.uncertainty(place.answers_count) * shift))
  
          self.predictions[answer.id] = prediction
  
      def train(self, data):
          """Trains the model on given data set.
  
          :param data: Data set on which to train the model.
          :type data: :class:`pandas.DataFrame`
          """
          self.init_model()
          data = tools.first_answers(data)
          data.sort(['inserted']).apply(self.update, axis=1)
  
      @classmethod
      def split_data(cls, data, ratio=0.7):
          """Classmethod that splits data into training set and test set.
  
          :param data: The object containing data.
          :type data: :class:`pandas.DataFrame`.
          :param ratio: What portion of data to include in the training set
              and the test set. :obj:`0.5` means that the data will be
              distributed equaly.
          :type ratio: float
          """
          data = tools.first_answers(data)
          return tools.split_data(data, ratio=ratio)
  
  
  class EloResponseTime(EloModel):
      """Extension of the Elo model that takes response time of user
      into account.
      """
      ABBR = 'Elo/RT'
  
      def __init__(self, *args, **kwargs):
          self.zeta = kwargs.pop('zeta', 3)
  
          super(EloResponseTime, self).__init__(*args, **kwargs)
  
      def update(self, answer):
          """Updates skills of users and difficulties of places according
          to given answer.
  
          :param answer: Answer to a question.
          :type answer: :class:`pandas.Series` or :class:`Answer`
          """
          user = self.users[answer.user_id]
          place = self.places[answer.place_id]
  
          prediction = self.predict(answer)
          level = tools.automaticity_level(answer.response_time)
  
          prob = (prediction * self.zeta + level) / (self.zeta + 1)
          shift = answer.is_correct - prob
  
          user.inc_skill(self.uncertainty(user.answers_count) * shift)
          place.inc_difficulty(-(self.uncertainty(place.answers_count) * shift))
  
          self.predictions[answer.id] = prediction
  
  
  class PFAModel(Model):
      """Standard Performance Factor Analysis.
  
      :param gamma: The significance of the update when the student
          answered correctly.
      :type gamma: float
      :param delta: The significance of the update when the student
          answered incorrectly.
      :type delta: float
      """
      ABBR = 'PFA'
  
      def __init__(self, prior=None, gamma=3.4, delta=-0.3):
          super(PFAModel, self).__init__()
  
          self.prior = prior or DummyPriorModel()
          self.gamma = gamma
          self.delta = delta
  
          self.init_model()
  
      def init_model(self):
          """Initializes attribute of the model that stores current
          knowledge of places for all students.
          """
          self.items = tools.keydefaultdict(
              lambda *args: Item(self.prior, *args)
          )
          self.predictions = {}
  
      def predict(self, question):
          """Returns probability of correct answer for given question.
  
          :param question: Asked question.
          :type question: :class:`pandas.Series` or :class:`Question`
          """
          item = self.items[question.user_id, question.place_id]
  
          knowledge = (
              item.knowledge +
              self.gamma * len(item.correct) +
              self.delta * len(item.incorrect)
          )
  
          return tools.sigmoid(knowledge)
  
      def update(self, answer):
          """Performes update of current knowledge of a user based on the
          given answer.
  
          :param answer: Answer to a question.
          :type answer: :class:`pandas.Series` or :class:`Answer`
          """
          item = self.items[answer.user_id, answer.place_id]
  
          if not item.practices:
              self.prior.update(answer)
  
          prediction = self.predict(answer)
          self.predictions[answer.id] = prediction
  
          item.add_practice(answer)
  
      def train(self, data):
          """Trains the model on given data set.
  
          :param data: Data set on which to train the model.
          :type data: :class:`pandas.DataFrame`
          """
          self.init_model()
          data.sort(['inserted']).apply(self.update, axis=1)
  
      @classmethod
      def split_data(self, data):
          """Classmethod that splits data into training set and test set.
  
          :param data: The object containing data.
          :type data: :class:`pandas.DataFrame`.
          """
          test_set = tools.last_answers(data)
          train_set = data[~data['id'].isin(test_set['id'])]
  
          return train_set, test_set
  
  
  class PFAExt(PFAModel):
      """PFA model for estimation of current knowledge.
  
      :param gamma: The significance of the update when the student
          answered correctly.
      :type gamma: float
      :param delta: The significance of the update when the student
          answered incorrectly.
      :type delta: float
      """
      ABBR = 'PFA/E'
  
      def predict(self, question):
          """Returns probability of correct answer for given question.
  
          :param question: Asked question.
          :type question: :class:`pandas.Series` or :class:`Question`
          """
          item = self.items[question.user_id, question.place_id]
          prediction = tools.sigmoid(item.knowledge)
          return self.respect_guess(prediction, question.options)
  
      def update(self, answer):
          """Performes update of current knowledge of a user based on the
          given answer.
  
          :param answer: Answer to a question.
          :type answer: :class:`pandas.Series` or :class:`Answer`
          """
          item = self.items[answer.user_id, answer.place_id]
  
          if not item.practices:
              self.prior.update(answer)
  
          prediction = self.predict(answer)
          self.predictions[answer.id] = prediction
  
          item.add_practice(answer)
  
          if answer.is_correct:
              item.inc_knowledge(self.gamma * (1 - prediction))
          else:
              item.inc_knowledge(self.delta * prediction)
  
  
  class PFAResponseTime(PFAExt):
      """An extended version of the PFAExt model which alters student's
      knowledge by respecting past response times.
  
      :param gamma: The significance of the update when the student
          answered correctly.
      :type gamma: float
      :param delta: The significance of the update when the student
          answered incorrectly.
      :type delta: float
      :param zeta: The significance of response times.
      :type zeta: float
      """
      ABBR = 'PFA/E/RT'
  
      def __init__(self, *args, **kwargs):
          kwargs.setdefault('gamma', 1.5)
          kwargs.setdefault('delta', -1.4)
  
          self.zeta = kwargs.pop('zeta', 1.9)
  
          super(PFAResponseTime, self).__init__(*args, **kwargs)
  
      def update(self, answer):
          """Performes update of current knowledge of a user based on the
          given answer.
  
          :param answer: Answer to a question.
          :type answer: :class:`pandas.Series` or :class:`Answer`
          """
          item = self.items[answer.user_id, answer.place_id]
  
          if not item.practices:
              self.prior.update(answer)
  
          prediction = self.predict(answer)
          self.predictions[answer.id] = prediction
  
          item.add_practice(answer)
          level = tools.automaticity_level(answer.response_time) / self.zeta
  
          if answer.is_correct:
              item.inc_knowledge(self.gamma * (1 - prediction) + level)
          else:
              item.inc_knowledge(self.delta * prediction + level)
  
  
  class PFAExtTiming(PFAExt):
      """Alternative version of :class:`PFAExtSpacing` which ignores
      spacing effect. Only forgetting is considered.
  
      :param gamma: The significance of the update when the student
          answered correctly.
      :type gamma: float
      :param delta: The significance of the update when the student
          answered incorrectly.
      :type delta: float
      :param time_effect_fun: Time effect function.
      :type time_effect_fun: callable or string
      """
      ABBR = 'PFA/E/T'
  
      def __init__(self, *args, **kwargs):
          kwargs.setdefault('gamma', 2.3)
          kwargs.setdefault('delta', -0.9)
  
          time_effect = kwargs.pop('time_effect_fun', 'poly')
  
          if isinstance(time_effect, basestring):
              self.a, self.c = kwargs.pop('a', None), kwargs.pop('c', None)
              self.time_effect = init_time_effect(self, time_effect)
          else:
              self.time_effect = time_effect
  
          super(PFAExtTiming, self).__init__(*args, **kwargs)
  
      def predict(self, question):
          """Returns probability of correct answer for given question.
  
          :param question: Asked question.
          :type question: :class:`pandas.Series` or :class:`Question`
          """
          item = self.items[question.user_id, question.place_id]
  
          if item.practices:
              seconds = tools.time_diff(question.inserted, item.last_inserted)
              time_effect = self.time_effect(seconds)
          else:
              time_effect = 0
  
          prediction = tools.sigmoid(item.knowledge + time_effect)
          return self.respect_guess(prediction, question.options)
  
  
  class PFAExtStaircase(PFAExtTiming):
      """Alternative version of :class:`PFAESpacing` which ignores
      spacing effect. Only forgetting is considered given by staircase
      fucntion.
  
      :param gamma: The significance of the update when the student
          answered correctly.
      :type gamma: float
      :param delta: The significance of the update when the student
          answered incorrectly.
      :type delta: float
      :param time_effect_fun: Values for staircase function.
      :type time_effect_fun: dict (tuples as keys)
      """
      ABBR = 'PFA/E/T staircase'
  
      def __init__(self, *args, **kwargs):
          kwargs.setdefault('gamma', 2.5)
          kwargs.setdefault('delta', -0.8)
  
          self.staircase = tools.intervaldict(kwargs.pop('staircase'))
          self.time_effect = lambda k: self.staircase[k]
  
          super(PFAExtTiming, self).__init__(*args, **kwargs)
  
  
  class PFAExtSpacing(PFAExtTiming):
      """Extended version of PFA that takes into account the effect of
      forgetting and spacing.
  
      :param gamma: The significance of the update when the student
          answers correctly.
      :type gamma: float
      :param delta: The significance of the update when the student
          answers incorrectly.
      :type delta: float
      :param spacing_rate: The significance of the spacing effect. Lower
          values make the effect less significant. If the spacing rate
          is set to zero, the model is unaware of the spacing effect.
      :type spacing_rate: float
      :param decay_rate: The significance of the forgetting effect. Higher
          values of decay rate make the students forget the item faster
          and vice versa.
      :type decay_rate: float
      """
      ABBR = 'PFA/E/S'
  
      def __init__(self, *args, **kwargs):
          kwargs.setdefault('gamma', 2.8)
          kwargs.setdefault('delta', -0.7)
  
          self.spacing_rate = kwargs.pop('spacing_rate', 0)
          self.decay_rate = kwargs.pop('decay_rate', 0.18)
          self.iota = kwargs.pop('iota', 1.5)
  
          super(PFAExtSpacing, self).__init__(*args, **kwargs)
  
      def memory_strength(self, question):
          """Estimates memory strength of an item.
  
          :param question: Asked question.
          :type question: :class:`pandas.Series`
          """
          item = self.items[question.user_id, question.place_id]
          practices = item.get_diffs(question.inserted)
  
          if len(practices) > 0:
              return self.iota + tools.memory_strength(
                  filter(lambda x: x > 0, practices),
                  spacing_rate=self.spacing_rate,
                  decay_rate=self.decay_rate,
              )
  
      def predict(self, question):
          """Returns probability of correct answer for given question.
  
          :param question: Asked question.
          :type question: :class:`pandas.Series` or :class:`Question`
          """
          item = self.items[question.user_id, question.place_id]
  
          if item.any_incorrect:
              strength = self.memory_strength(question)
          else:
              strength = 0
  
          prediction = tools.sigmoid(item.knowledge + strength)
          return self.respect_guess(prediction, question.options)
  
  
  class PFAGong(PFAModel):
      """Yue Gong's extended Performance Factor Analysis.
  
      :param gamma: The significance of the update when the student
          answers correctly.
      :type gamma: float
      :param delta: The significance of the update when the student
          answers incorrectly.
      :type delta: float
      :param decay: Decay rate of answers.
      :type decay: float
      """
      ABBR = 'PFA/G'
  
      def __init__(self, *args, **kwargs):
          kwargs.setdefault('gamma', 2.1)
          kwargs.setdefault('delta', -0.8)
  
          self.decay = kwargs.pop('decay', 0.8)
  
          super(PFAGong, self).__init__(*args, **kwargs)
  
      def get_weights(self, item, question):
          """Returns weights of previous answers to the given item.
  
          :param item: *Item* (i.e. practiced place by a user).
          :type item: :class:`Item`
          :param question: Asked question.
          :type question: :class:`pandas.Series` or :class:`Question`
          """
          correct_weights = [
              ans.is_correct * self.decay ** k for k, ans
              in tools.reverse_enumerate(item.practices)
          ]
          incorrect_weights = [
              (1 - ans.is_correct) * self.decay ** k for k, ans
              in tools.reverse_enumerate(item.practices)
          ]
          return sum(correct_weights), sum(incorrect_weights)
  
      def predict(self, question):
          """Returns probability of correct answer for given question.
  
          :param question: Asked question.
          :type question: :class:`pandas.Series` or :class:`Question`
          """
          item = self.items[question.user_id, question.place_id]
          correct_weight, incorrect_weight = self.get_weights(item, question)
  
          knowledge = (
              item.knowledge +
              self.gamma * correct_weight +
              self.delta * incorrect_weight
          )
  
          prediction = tools.sigmoid(knowledge)
          return self.respect_guess(prediction, question.options)
  
      def update(self, answer):
          """Performes update of current knowledge of a user based on the
          given answer.
  
          :param answer: Answer to a question.
          :type answer: :class:`pandas.Series` or :class:`Answer`
          """
          item = self.items[answer.user_id, answer.place_id]
  
          if not item.practices:
              self.prior.update(answer)
  
          prediction = self.predict(answer)
          self.predictions[answer.id] = prediction
  
          item.add_practice(answer)
  
  
  class PFAGongTiming(PFAGong):
      """Performance Factor Analysis combining some aspects of both
      the Yue Gong's PFA and the ACT-R model.
  
      :param gamma: The significance of the update when the student
          answers correctly.
      :type gamma: float
      :param delta: The significance of the update when the student
          answers incorrectly.
      :type delta: float
      :param time_effect_fun: Time effect function.
      :type time_effect_fun: callable or string
      """
      ABBR = 'PFA/G/T old'
  
      def __init__(self, *args, **kwargs):
          kwargs.setdefault('gamma', 1.7)
          kwargs.setdefault('delta', 0.5)
  
          time_effect = kwargs.pop('time_effect_fun', 'pow')
  
          if isinstance(time_effect, basestring):
              self.a, self.c = kwargs.pop('a', None), kwargs.pop('c', None)
              self.time_effect = init_time_effect(self, time_effect)
          else:
              self.time_effect = time_effect
  
          super(PFAGong, self).__init__(*args, **kwargs)
  
      def get_weights(self, item, question):
          """Returns weights of previous answers to the given item.
  
          :param item: *Item* (i.e. practiced place by a user).
          :type item: :class:`Item`
          :param question: Asked question.
          :type question: :class:`pandas.Series` or :class:`Question`
          """
          correct_weights = [
              max(ans.is_correct * self.time_effect(diff), 0) for ans, diff
              in izip(item.practices, item.get_diffs(question.inserted))
          ]
          incorrect_weights = [
              (1 - ans.is_correct) * self.time_effect(diff) for ans, diff
              in izip(item.practices, item.get_diffs(question.inserted))
          ]
          return sum(correct_weights), sum(incorrect_weights)
  
  
  class PFATiming(PFAGong):
      """Performance Factor Analysis combining some aspects of both
      the Yue Gong's PFA and the ACT-R model.
  
      :param gamma: The significance of the update when the student
          answers correctly.
      :type gamma: float
      :param delta: The significance of the update when the student
          answers incorrectly.
      :type delta: float
      :param time_effect_good: Time effect function for correct answers.
      :type time_effect_good: callable or string
      :param time_effect_bad: Time effect function for wrong answers.
      :type time_effect_bad: callable or string
      """
      ABBR = 'PFA/G/T'
  
      def __init__(self, *args, **kwargs):
          kwargs.setdefault('gamma', 1)  # these parameters should not be
          kwargs.setdefault('delta', 1)  # modified, i.e. kept equal to 1
  
          time_effect_good = kwargs.pop('time_effect_good', 'pow')
          time_effect_bad = kwargs.pop('time_effect_bad', 'pow')
  
          if isinstance(time_effect_good, basestring):
              self.a, self.c = kwargs.pop('a', None), kwargs.pop('c', None)
              self.time_effect_good = init_time_effect(
                  self, time_effect_good, parameters=('a', 'c'))
          else:
              self.time_effect_good = time_effect_good
  
          if isinstance(time_effect_bad, basestring):
              self.b, self.d = kwargs.pop('b', None), kwargs.pop('d', None)
              self.time_effect_bad = init_time_effect(
                  self, time_effect_bad, parameters=('b', 'd'))
          else:
              self.time_effect_bad = time_effect_bad
  
          super(PFAGong, self).__init__(*args, **kwargs)
  
      def get_weights(self, item, question):
          """Returns weights of previous answers to the given item.
  
          :param item: *Item* (i.e. practiced place by a user).
          :type item: :class:`Item`
          :param question: Asked question.
          :type question: :class:`pandas.Series` or :class:`Question`
          """
          correct_weights = [
              ans.is_correct * self.time_effect_good(diff) for ans, diff
              in izip(item.practices, item.get_diffs(question.inserted))
          ]
          incorrect_weights = [
              (1 - ans.is_correct) * self.time_effect_bad(diff) for ans, diff
              in izip(item.practices, item.get_diffs(question.inserted))
          ]
          return sum(correct_weights), sum(incorrect_weights)
  