Repository: nltk/nltk

Issue:
decisiontree classify demonstration is broken
$ python ./classify/decisiontree.py
Training classifier...
Traceback (most recent call last):
  File "./decisiontree.py", line 285, in <module>
    demo()
  File "./decisiontree.py", line 280, in demo
    binary_names_demo_features)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/nltk-2.0.3-py2.6.egg/nltk/classify/util.py", line 182, in names_demo
    classifier = trainer( [(features(n), g) for (n,g) in train] )
  File "./decisiontree.py", line 275, in f
    return DecisionTreeClassifier.train(x, binary=True, verbose=True)
  File "./decisiontree.py", line 155, in train
    feature_names, labeled_featuresets, feature_values, verbose)
  File "./decisiontree.py", line 255, in best_binary_stump
    fname, fval, labeled_featuresets)
  File "./decisiontree.py", line 244, in binary_stump
    default = DecisionTreeClassifier(neg_fdist.max())
  File "/opt/local/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/nltk-2.0.3-py2.6.egg/nltk/probability.py", line 270, in max
    raise ValueError('A FreqDist must have at least one sample before max is defined.')
ValueError: A FreqDist must have at least one sample before max is defined.



Discussion:
Just taking out "binary=True" over here:
https://github.com/nltk/nltk/blob/master/nltk/classify/decisiontree.py#L277

... makes it work. But I think the underlying bug is in binary_stump -- we mess it up trying to get the max of those FreqDists when they have no observations. But then what's the right thing to do if you get into that function and you have no applicable observations? ...

(working on it...)


Resolving patch:
diff --git a/nltk/classify/decisiontree.py b/nltk/classify/decisiontree.py
--- a/nltk/classify/decisiontree.py
+++ b/nltk/classify/decisiontree.py
@@ -114,7 +114,7 @@ def pseudocode(self, prefix='', depth=4):
         if self._default is not None:
             if len(self._decisions) == 1:
                 s += '%sif %s != %r: '% (prefix, self._fname,
-                                         self._decisions.keys()[0])
+                                         list(self._decisions.keys())[0])
             else:
                 s += '%selse: ' % (prefix,)
             if self._default._fname is not None and depth>1:
@@ -131,7 +131,7 @@ def train(labeled_featuresets, entropy_cutoff=0.05, depth_cutoff=100,
               support_cutoff=10, binary=False, feature_values=None,
               verbose=False):
         """
-        :param binary: If true, then treat all feature/value pairs a
+        :param binary: If true, then treat all feature/value pairs as
             individual binary features, rather than using a single n-way
             branch for each feature.
         """
@@ -242,8 +242,15 @@ def binary_stump(feature_name, feature_value, labeled_featuresets):
             else:
                 neg_fdist.inc(label)
 
-        decisions = {feature_value: DecisionTreeClassifier(pos_fdist.max())}
-        default = DecisionTreeClassifier(neg_fdist.max())
+
+        decisions = {}
+        default = label
+        # But hopefully we have observations!
+        if pos_fdist.N() > 0:
+            decisions = {feature_value: DecisionTreeClassifier(pos_fdist.max())}
+        if neg_fdist.N() > 0:
+            default = DecisionTreeClassifier(neg_fdist.max())
+
         return DecisionTreeClassifier(label, feature_name, decisions, default)
 
     @staticmethod
@@ -261,7 +268,7 @@ def best_binary_stump(feature_names, labeled_featuresets, feature_values,
                     best_stump = stump
         if best_stump._decisions:
             descr = '%s=%s' % (best_stump._fname,
-                               best_stump._decisions.keys()[0])
+                               list(best_stump._decisions.keys())[0])
         else:
             descr = '(default)'
         if verbose:


Test patch: