source: orange/Orange/classification/lookup.py @ 10090:5f656d83dfd8

Revision 10090:5f656d83dfd8, 24.3 KB checked in by Jure Zbontar <jure.zbontar@…>, 2 years ago (diff)

Fixed doctests in lookup

Line 
1"""
2
3.. index:: classification; lookup
4
5*******************************
6Lookup classifiers (``lookup``)
7*******************************
8
9Lookup classifiers predict classes by looking into stored lists of
10cases. There are two kinds of such classifiers in Orange. The simpler
11and faster :obj:`ClassifierByLookupTable` uses up to three discrete
12features and has a stored mapping from values of those features to the
13class value. The more complex classifiers store an
14:obj:`Orange.data.Table` and predict the class by matching the instance
15to instances in the table.
16
17.. index::
18   single: feature construction; lookup classifiers
19
20A natural habitat for these classifiers is feature construction:
21they usually reside in :obj:`~Orange.feature.Descriptor.get_value_from`
22fields of constructed
23features to facilitate their automatic computation. For instance,
24the following script shows how to translate the ``monks-1.tab`` data set
25features into a more useful subset that will only include the features
26``a``, ``b``, ``e``, and features that will tell whether ``a`` and ``b``
27are equal and whether ``e`` is 1 (part of
28:download:`lookup-lookup.py <code/lookup-lookup.py>`):
29
30..
31    .. literalinclude:: code/lookup-lookup.py
32        :lines: 7-21
33
34.. testcode::
35
36    import Orange
37
38    monks = Orange.data.Table("monks-1")
39
40    a, b, e = monks.domain["a"], monks.domain["b"], monks.domain["e"]
41
42    ab = Orange.feature.Discrete("a==b", values = ["no", "yes"])
43    ab.get_value_from = Orange.classification.lookup.ClassifierByLookupTable(ab, a, b,
44                        ["yes", "no", "no",  "no", "yes", "no",  "no", "no", "yes"])
45
46    e1 = Orange.feature.Discrete("e==1", values = ["no", "yes"])
47    e1.get_value_from = Orange.classification.lookup.ClassifierByLookupTable(e1, e,
48                        ["yes", "no", "no", "no", "?"])
49
50    monks2 = monks.select([a, b, ab, e, e1, monks.domain.class_var])
51   
52We can check the correctness of the script by printing out several
53random examples from ``data2``.
54
55    >>> for i in range(5):
56    ...     print monks2.randomexample()
57    ['3', '2', 'no', '2', 'no', '0']
58    ['2', '2', 'yes', '2', 'no', '1']
59    ['1', '2', 'no', '2', 'no', '0']
60    ['2', '3', 'no', '1', 'yes', '1']
61    ['1', '3', 'no', '1', 'yes', '1']
62
63The first :obj:`ClassifierByLookupTable` takes values of features ``a``
64and ``b`` and computes the value of ``ab`` according to the rule given in the
65given table. The first three values correspond to ``a=1`` and ``b=1,2,3``;
66for the first combination, value of ``ab`` should be "yes", for the other
67two ``a`` and ``b`` are different. The next triplet corresponds to ``a=2``;
68here, the middle value is "yes"...
69
70The second lookup is simpler: since it involves only a single feature,
71the list is a simple one-to-one mapping from the four-valued ``e`` to the
72two-valued ``e1``. The last value in the list is returned when ``e`` is unknown
73and tells that ``e1`` should be unknown then as well.
74
75Note that :obj:`ClassifierByLookupTable` is not needed for this.
76The new feature ``e1`` could be computed with a callback to Python,
77for instance::
78
79    e2.get_value_from = lambda ex, rw: orange.Value(e2, ex["e"] == "1")
80
81
82Classifiers by lookup table
83===========================
84
85.. index::
86   single: classification; lookup table
87
88Although the above example used :obj:`ClassifierByLookupTable` as if it
89was a concrete class, :obj:`ClassifierByLookupTable` is actually
90abstract. Calling its constructor is a typical Orange trick: it does not
91return an instance of :obj:`ClassifierByLookupTable`, but either
92:obj:`ClassifierByLookupTable1`, :obj:`ClassifierByLookupTable2` or
93:obj:`ClassifierByLookupTable3`. As their names tell, the first
94classifies using a single feature (so that's what we had for ``e1``),
95the second uses a pair of features (and has been constructed for ``ab``
96above), and the third uses three features. Class predictions for each
97combination of feature values are stored in a (one dimensional) table.
98To classify an instance, the classifier computes an index of the element
99of the table that corresponds to the combination of feature values.
100
101These classifiers are built to be fast, not safe. For instance, if the number
102of values for one of the features is changed, Orange will most probably crash.
103To alleviate this, many of these classes' features are read-only and can only
104be set when the object is constructed.
105
106
107.. py:class:: ClassifierByLookupTable(class_var, variable1[, variable2[, variable3]] [, lookup_table[, distributions]])
108   
109    A general constructor that, based on the number of feature descriptors,
110    constructs one of the three classes discussed. If :obj:`lookup_table`
111    and :obj:`distributions` are omitted, the constructor also initializes
112    them to two lists of the right sizes, but their elements are don't knows
113    and empty distributions. If they are given, they must be of correct size.
114   
115    .. attribute:: variable1[, variable2[, variable3]](read only)
116       
117        The feature(s) that the classifier uses for classification.
118        ClassifierByLookupTable1 only has variable1,
119        ClassifierByLookupTable2 also has variable2 and
120        ClassifierByLookupTable3 has all three.
121
122    .. attribute:: variables (read only)
123       
124        The above variables, returned as a tuple.
125
126    .. attribute:: no_of_values1[, no_of_values2[, no_of_values3]] (read only)
127       
128        The number of values for variable1, variable2 and variable3.
129        This is stored here to make the classifier faster. Those features
130        are defined only for ClassifierByLookupTable2 (the first two) and
131        ClassifierByLookupTable3 (all three).
132
133    .. attribute:: lookup_table (read only)
134       
135        A list of values, one for each possible
136        combination of features. For ClassifierByLookupTable1, there is an
137        additional element that is returned when the feature's value is
138        unknown. Values are ordered by values of features, with variable1
139        being the most important. In case of two three valued features, the
140        list order is therefore 1-1, 1-2, 1-3, 2-1, 2-2, 2-3, 3-1, 3-2, 3-3,
141        where the first digit corresponds to variable1 and the second to
142        variable2.
143       
144        The attribute is read-only - a new list cannot be assigned to it.
145        Its elements, however, can be changed. Don't change its size.
146
147    .. attribute:: distributions (read only)
148       
149        Similar to :obj:`lookup_table`, but it stores a distribution for
150        each combination of values.
151
152    .. attribute:: data_description
153       
154        An object of type :obj:`EFMDataDescription`, defined only for
155        ClassifierByLookupTable2 and ClassifierByLookupTable3. They use it
156        to make predictions when one or more feature values are unknown.
157        ClassifierByLookupTable1 doesn't need it since this case is covered by
158        an additional element in :obj:`lookup_table` and :obj:`distributions`,
159        as told above.
160       
161    .. method:: get_index(example)
162   
163        Returns an index of ``example`` in :obj:`lookup_table` and
164        :obj:`distributions`. The formula depends upon the type of
165        the classifier. If value\ *i* is int(example[variable\ *i*]),
166        then the corresponding formulae are
167
168        ClassifierByLookupTable1:
169            index = value1, or len(lookup_table) - 1 if value is unknown
170        ClassifierByLookupTable2:
171            index = value1 * no_of_values1 + value2, or -1 if any value is unknown
172        ClassifierByLookupTable3:
173            index = (value1 * no_of_values1 + value2) * no_of_values2 + value3, or -1 if any value is unknown
174
175        Let's see some indices for randomly chosen examples from the original table.
176       
177        part of :download:`lookup-lookup.py <code/lookup-lookup.py>`:
178
179        .. literalinclude:: code/lookup-lookup.py
180            :lines: 26-29
181       
182        Output::
183       
184            ['3', '2', '1', '2', '2', '1', '0']: ab 7, e1 1
185            ['2', '2', '1', '2', '2', '1', '1']: ab 4, e1 1
186            ['1', '2', '1', '2', '2', '2', '0']: ab 1, e1 1
187            ['2', '3', '2', '3', '1', '1', '1']: ab 5, e1 0
188            ['1', '3', '2', '2', '1', '1', '1']: ab 2, e1 0
189
190
191
192.. py:class:: ClassifierByLookupTable1(class_var, variable1 [, lookup_table, distributions])
193   
194    Uses a single feature for lookup. See
195    :obj:`ClassifierByLookupTable` for more details.
196
197.. py:class:: ClassifierByLookupTable2(class_var, variable1, variable2, [, lookup_table[, distributions]])
198   
199    Uses two features for lookup. See
200    :obj:`ClassifierByLookupTable` for more details.
201       
202.. py:class:: ClassifierByLookupTable3(class_var, variable1, variable2, variable3, [, lookup_table[, distributions]])
203   
204    Uses three features for lookup. See
205    :obj:`ClassifierByLookupTable` for more details.
206
207
208Classifier by data table
209========================
210
211.. index::
212   single: classification; data table
213
214:obj:`ClassifierByDataTable` is used in similar contexts as
215:obj:`ClassifierByLookupTable`. If you write, for instance, a
216constructive induction algorithm, it is recommended that the values
217of the new feature are computed either by one of classifiers by lookup
218table or by ClassifierByDataTable, depending on the number of bound
219features.
220
221.. py:class:: ClassifierByDataTable
222
223    :obj:`ClassifierByDataTable` is the alternative to
224    :obj:`ClassifierByLookupTable`. It is to be used when the
225    classification is based on more than three features. Instead of having
226    a lookup table, it stores an :obj:`Orange.data.Table`, which is
227    optimized for a faster access.
228   
229
230    .. attribute:: sorted_examples
231       
232        A :obj:`Orange.data.Table` with sorted data instances for lookup.
233        Instances in the table can be merged; if there were multiple
234        instances with the same feature values (but possibly different
235        classes), they are merged into a single instance. Regardless of
236        merging, class values in this table are distributed: their svalue
237        contains a :obj:`~Orange.statistics.distribution.Distribution`.
238
239    .. attribute:: classifier_for_unknown
240       
241        This classifier is used to classify instances which were not found
242        in the table. If classifier_for_unknown is not set, don't know's are
243        returned.
244
245    .. attribute:: variables (read only)
246       
247        A tuple with features in the domain. This field is here so that
248        :obj:`ClassifierByDataTable` appears more similar to
249        :obj:`ClassifierByLookupTable`. If a constructive induction
250        algorithm returns the result in one of these classifiers, and you
251        would like to check which features are used, you can use variables
252        regardless of the class you actually got.
253
254    There are no specific methods for ClassifierByDataTable.
255    Since this is a classifier, it can be called. When the instance to be
256    classified includes unknown values, :obj:`classifier_for_unknown` will be
257    used if it is defined.
258
259
260
261.. py:class:: LookupLearner
262   
263    Although :obj:`ClassifierByDataTable` is not really a classifier in
264    the sense that you will use it to classify instances, but is rather a
265    function for computation of intermediate values, it has an associated
266    learner, :obj:`LookupLearner`. The learner's task is, basically, to
267    construct a table for :obj:`ClassifierByDataTable.sorted_examples`.
268    It sorts them, merges them
269    and regards instance weights in the process as well.
270   
271    If data instances are provided to the constructor, the learning algorithm
272    is called and the resulting classifier is returned instead of the learner.
273
274part of :download:`lookup-table.py <code/lookup-table.py>`:
275
276..
277    .. literalinclude:: code/lookup-table.py
278        :lines: 7-13
279
280.. testcode::
281       
282    import Orange
283
284    table = Orange.data.Table("monks-1")
285    a, b, e = table.domain["a"], table.domain["b"], table.domain["e"]
286
287    table_s = table.select([a, b, e, table.domain.class_var])
288    abe = Orange.classification.lookup.LookupLearner(table_s)
289
290
291In ``table_s``, we have prepared a table in which instances are described
292only by ``a``, ``b``, ``e`` and the class. The learner constructs a
293:obj:`ClassifierByDataTable` and stores instances from ``table_s`` into its
294:obj:`~ClassifierByDataTable.sorted_examples`. Instances are merged so that
295there are no duplicates.
296
297    >>> print len(table_s)
298    556
299    >>> print len(abe.sorted_examples)
300    36
301    >>> for i in abe.sorted_examples[:10]:  # doctest: +SKIP
302    ...     print i
303    ['1', '1', '1', '1']
304    ['1', '1', '2', '1']
305    ['1', '1', '3', '1']
306    ['1', '1', '4', '1']
307    ['1', '2', '1', '1']
308    ['1', '2', '2', '0']
309    ['1', '2', '3', '0']
310    ['1', '2', '4', '0']
311    ['1', '3', '1', '1']
312    ['1', '3', '2', '0']
313
314Well, there's a bit more here than meets the eye: each instance's class
315value also stores the distribution of classes for all instances that
316were merged into it. In our case, the three features suffice to
317unambiguously determine the classes and, since instances covered the
318entire space, all distributions have 12 instances in one of the class
319and none in the other.
320
321    >>> for i in abe.sorted_examples[:10]:  # doctest: +SKIP
322    ...     print i, i.get_class().svalue
323    ['1', '1', '1', '1'] <0.000, 12.000>
324    ['1', '1', '2', '1'] <0.000, 12.000>
325    ['1', '1', '3', '1'] <0.000, 12.000>
326    ['1', '1', '4', '1'] <0.000, 12.000>
327    ['1', '2', '1', '1'] <0.000, 12.000>
328    ['1', '2', '2', '0'] <12.000, 0.000>
329    ['1', '2', '3', '0'] <12.000, 0.000>
330    ['1', '2', '4', '0'] <12.000, 0.000>
331    ['1', '3', '1', '1'] <0.000, 12.000>
332    ['1', '3', '2', '0'] <12.000, 0.000>
333
334:obj:`ClassifierByDataTable` will usually be used by
335:obj:`~Orange.feature.Descriptor.get_value_from`. So, we
336would probably continue this by constructing a new feature and put the
337classifier into its :obj:`~Orange.feature.Descriptor.get_value_from`.
338
339    >>> y2 = Orange.feature.Discrete("y2", values = ["0", "1"])
340    >>> y2.get_value_from = abe
341
342Although ``abe`` determines the value of ``y2``, ``abe.class_var`` is still ``y``.
343Orange doesn't mind (the whole example is artificial - the entire data set
344will seldom be packed in an :obj:`ClassifierByDataTable`), but this can still
345be solved by
346
347    >>> abe.class_var = y2
348
349The whole story can be greatly simplified. :obj:`LookupLearner` can also be
350called differently than other learners. Besides instances, you can pass
351the new class variable and the features that should be used for
352classification. This saves us from constructing table_s and reassigning
353the :obj:`~Orange.data.Domain.class_var`. It doesn't set the
354:obj:`~Orange.feature.Descriptor.get_value_from`, though.
355
356part of :download:`lookup-table.py <code/lookup-table.py>`::
357
358    import Orange
359
360    table = Orange.data.Table("monks-1")
361    a, b, e = table.domain["a"], table.domain["b"], table.domain["e"]
362
363    y2 = Orange.feature.Discrete("y2", values = ["0", "1"])
364    abe2 = Orange.classification.lookup.LookupLearner(y2, [a, b, e], table)
365
366Let us, for the end, show another use of :obj:`LookupLearner`. With the
367alternative call arguments, it offers an easy way to observe feature
368interactions. For this purpose, we shall omit ``e``, and construct a
369:obj:`ClassifierByDataTable` from ``a`` and ``b`` only (part of
370:download:`lookup-table.py <code/lookup-table.py>`):
371
372.. literalinclude:: code/lookup-table.py
373    :lines: 32-35
374
375The script's output show how the classes are distributed for different
376values of ``a`` and ``b``::
377
378    ['1', '1', '1'] <0.000, 48.000>
379    ['1', '2', '0'] <36.000, 12.000>
380    ['1', '3', '0'] <36.000, 12.000>
381    ['2', '1', '0'] <36.000, 12.000>
382    ['2', '2', '1'] <0.000, 48.000>
383    ['2', '3', '0'] <36.000, 12.000>
384    ['3', '1', '0'] <36.000, 12.000>
385    ['3', '2', '0'] <36.000, 12.000>
386    ['3', '3', '1'] <0.000, 48.000>
387
388For instance, when ``a`` is '1' and ``b`` is '3', the majority class is '0',
389and the class distribution is 36:12 in favor of '0'.
390
391
392Utility functions
393=================
394
395
396There are several functions for working with classifiers that use a stored
397data table for making predictions. There are four such classifiers; the most
398general stores a :class:`~Orange.data.Table` and the other three are
399specialized and optimized for cases where the domain contains only one, two or
400three features (besides the class variable).
401
402.. function:: lookup_from_bound(class_var, bound)
403
404    This function constructs an appropriate lookup classifier for one, two or
405    three features. If there are more, it returns None. The resulting
406    classifier is of type :obj:`ClassifierByLookupTable`,
407    :obj:`ClassifierByLookupTable2` or :obj:`ClassifierByLookupTable3`, with
408    ``class_var`` and bound set set as given.
409
410    For example, using the data set ``monks-1.tab``, to construct a new feature
411    from features ``a`` and ``b``, this function can be called as follows.
412   
413        >>> new_var = Orange.feature.Discrete()
414        >>> bound = [table.domain[name] for name in ["a", "b"]]
415        >>> lookup = Orange.classification.lookup.lookup_from_bound(new_var, bound)
416        >>> print lookup.lookup_table
417        <?, ?, ?, ?, ?, ?, ?, ?, ?>
418
419    Function ``lookup_from_bound`` does not initialize neither ``new_var`` nor
420    the lookup table...
421
422.. function:: lookup_from_function(class_var, bound, function)
423
424    ... and that's exactly where ``lookup_from_function`` differs from
425    :obj:`lookup_from_bound`. ``lookup_from_function`` first calls
426    :obj:`lookup_from_bound` and then uses the function to initialize the
427    lookup table. The other difference between this and the previous function
428    is that ``lookup_from_function`` also accepts bound sets with more than three
429    features. In this case, it construct a :obj:`ClassifierByDataTable`.
430
431    The function gets the values of features as integer indices and should
432    return an integer index of the "class value". The class value must be
433    properly initialized.
434
435    For exercise, let us construct a new feature called ``a=b`` whose value will
436    be "yes" when ``a`` and ``b`` are equal and "no" when they are not. We will then
437    add the feature to the data set.
438   
439        >>> bound = [table.domain[name] for name in ["a", "b"]]
440        >>> new_var = Orange.feature.Discrete("a=b", values=["no", "yes"])
441        >>> lookup = Orange.classification.lookup.lookup_from_function(new_var, bound, lambda x: x[0] == x[1])
442        >>> new_var.get_value_from = lookup
443        >>> import orngCI
444        >>> table2 = orngCI.addAnAttribute(new_var, table)
445        >>> for i in table2[:30]:
446        ...     print i
447        ['1', '1', '1', '1', '3', '1', 'yes', '1']
448        ['1', '1', '1', '1', '3', '2', 'yes', '1']
449        ['1', '1', '1', '3', '2', '1', 'yes', '1']
450        ...
451        ['1', '2', '1', '1', '1', '2', 'no', '1']
452        ['1', '2', '1', '1', '2', '1', 'no', '0']
453        ['1', '2', '1', '1', '3', '1', 'no', '0']
454        ...
455
456    The feature was inserted with use of ``orngCI.addAnAttribute``. By setting
457    ``new_var.get_value_from`` to ``lookup`` we state that when converting domains
458    (either when needed by ``addAnAttribute`` or at some other place), ``lookup``
459    should be used to compute ``new_var``'s value. (A bit off topic, but
460    important: you should never call
461    :obj:`~Orange.feature.Descriptor.get_value_from` directly, but always
462    through :obj:`~Orange.feature.Descriptor.compute_value`.)
463
464.. function:: lookup_from_data(examples [, weight])
465
466    This function takes a set of data instances (e.g. :obj:`Orange.data.Table`)
467    and turns it into a classifier. If there are one, two or three features and
468    no ambiguous examples (examples are ambiguous if they have same values of
469    features but with different class values), it will construct an appropriate
470    :obj:`ClassifierByLookupTable`. Otherwise, it will return an
471    :obj:`ClassifierByDataTable`.
472   
473        >>> lookup = Orange.classification.lookup.lookup_from_data(table)
474        >>> test_instance = Orange.data.Instance(table.domain, ['3', '2', '2', '3', '4', '1', '?'])
475        >>> lookup(test_instance)
476        <orange.Value 'y'='0'>
477   
478.. function:: dump_lookup_function(func)
479
480    ``dump_lookup_function`` returns a string with a lookup function in
481    tab-delimited format. Argument ``func`` can be any of the above-mentioned
482    classifiers or a feature whose
483    :obj:`~Orange.feature.Descriptor.get_value_from` points to one of such
484    classifiers.
485
486    For instance, if ``lookup`` is such as constructed in the example for
487    ``lookup_from_function``, it can be printed by::
488   
489        >>> print dump_lookup_function(lookup)
490        a      b      a=b
491        ------ ------ ------
492        1      1      yes
493        1      2      no
494        1      3      no
495        2      1      no
496        2      2      yes
497        2      3      no
498        3      1      no
499        3      2      no
500        3      3      yes
501
502"""
503
504from Orange.misc import deprecated_keywords
505import Orange.data
506from Orange.core import \
507        LookupLearner, \
508         ClassifierByLookupTable, \
509              ClassifierByLookupTable1, \
510              ClassifierByLookupTable2, \
511              ClassifierByLookupTable3, \
512              ClassifierByExampleTable as ClassifierByDataTable
513
514
515@deprecated_keywords({"attribute":"class_var"})
516def lookup_from_bound(class_var, bound):
517    if not len(bound):
518        raise TypeError, "no bound attributes"
519    elif len(bound) <= 3:
520        return [ClassifierByLookupTable, ClassifierByLookupTable2,
521                ClassifierByLookupTable3][len(bound) - 1](class_var, *list(bound))
522    else:
523        return None
524
525   
526@deprecated_keywords({"attribute":"class_var"})
527def lookup_from_function(class_var, bound, function):
528    """
529    Constructs ClassifierByDataTable or ClassifierByLookupTable
530    mirroring the given function.
531   
532    """
533    lookup = lookup_from_bound(class_var, bound)
534    if lookup:
535        for i, attrs in enumerate(Orange.misc.counters.LimitedCounter(
536                    [len(var.values) for var in bound])):
537            lookup.lookup_table[i] = Orange.data.Value(class_var, function(attrs))
538        return lookup
539    else:
540        dom = Orange.data.Domain(bound, class_var)
541        data = Orange.data.Table(dom)
542        for attrs in Orange.misc.counters.LimitedCounter(
543                    [len(var.values) for var in dom.features]):
544            data.append(Orange.data.Example(dom, attrs + [function(attrs)]))
545        return LookupLearner(data)
546     
547
548@deprecated_keywords({"learnerForUnknown":"learner_for_unknown"})
549def lookup_from_data(examples, weight=0, learner_for_unknown=None):
550    if len(examples.domain.features) <= 3:
551        lookup = lookup_from_bound(examples.domain.class_var,
552                                 examples.domain.features)
553        lookup_table = lookup.lookup_table
554        for example in examples:
555            ind = lookup.get_index(example)
556            if not lookup_table[ind].is_special() and (lookup_table[ind] !=
557                                                     example.get_class()):
558                break
559            lookup_table[ind] = example.get_class()
560        else:
561            return lookup
562
563        # there are ambiguities; a backup plan is
564        # ClassifierByDataTable, let it deal with them
565        return LookupLearner(examples, weight,
566                             learner_for_unknown=learner_for_unknown)
567
568    else:
569        return LookupLearner(examples, weight,
570                             learner_for_unknown=learner_for_unknown)
571       
572       
573def dump_lookup_function(func):
574    if isinstance(func, Orange.feature.Descriptor):
575        if not func.get_value_from:
576            raise TypeError, "attribute '%s' does not have an associated function" % func.name
577        else:
578            func = func.get_value_from
579
580    outp = ""
581    if isinstance(func, ClassifierByDataTable):
582    # XXX This needs some polishing :-)
583        for i in func.sorted_examples:
584            outp += "%s\n" % i
585    else:
586        boundset = func.boundset()
587        for a in boundset:
588            outp += "%s\t" % a.name
589        outp += "%s\n" % func.class_var.name
590        outp += "------\t" * (len(boundset)+1) + "\n"
591       
592        lc = 0
593        if len(boundset)==1:
594            cnt = Orange.misc.counters.LimitedCounter([len(x.values)+1 for x in boundset])
595        else:
596            cnt = Orange.misc.counters.LimitedCounter([len(x.values) for x in boundset])
597        for ex in cnt:
598            for i in range(len(ex)):
599                if ex[i] < len(boundset[i].values):
600                    outp += "%s\t" % boundset[i].values[ex[i]]
601                else:
602                    outp += "?\t",
603            outp += "%s\n" % func.class_var.values[int(func.lookup_table[lc])]
604            lc += 1
605    return outp
Note: See TracBrowser for help on using the repository browser.