4
\$\begingroup\$

I am learning about the class and methods in Python.

The class Accuracy is a class of several (13 in total) statistic values between a reference polygon and one or more segmented polygons based on shapely module.

enter image description here

from numpy import average

#some stat

def ra_or(ref, seg):
    return average([(ref.intersection(s).area/ref.area) for s in seg])


def ra_os(ref, seg):
    return average([(ref.intersection(s).area/s.area) for s in seg])


def sim_size(ref, seg):
return average([(min(ref.area, s.area)/max(ref.area,s.area)) for s in seg])


def AFI(ref,seg):
   return (ref.area - max([s.area for s in seg]))/ref.area

where ref.intersection(s).area is the area of intersection between reference and segmented polygon-i

my class design (really basic and probably to improve) is:

class Accuracy(object):
    def __init__ (self,ref,seg = None, noData = -9999):
        if seg == None:
            self.area = ref.area
            self.perimeter = ref.length
            self.centroidX = ref.centroid.x
            self.centroidY = ref.centroid.y
            self.data =  [self.centroidX,
                        self.centroidY,
                        self.area,
                        self.perimeter,
                        noData,
                        noData,
                        noData,
                        noData,
                        noData]
        else:
            self.area = ref.area
            self.perimeter = ref.length
            self.centroidX = ref.centroid.x
            self.centroidY = ref.centroid.y
            self.segments = len(seg)
            self.RAor = ra_or(ref,seg)
            self.RAos = ra_os(ref,seg)
            self.SimSize = sim_size(ref,seg)
            self.AFI = AFI(ref,seg)
            self.data = [self.centroidX,
                        self.centroidY,
                        self.area,
                        self.perimeter,
                        self.segments,
                        self.RAor,
                        self.RAos,
                        self.SimSize,
                        self.AFI]

from shapely.geometry import Polygon

p1 = Polygon([(2, 4), (4, 4), (4, 2), (2, 2), (2, 4)])
p2 = Polygon([(0, 3), (3, 3), (3, 0), (0, 0), (0, 3)])

accp1 = Accuracy(p1,[p2])
accp1.data
[3.0, 3.0, 4.0, 8.0, 1, 0.25, 0.1111111111111111, 0.44444444444444442, -1.25]

accp1 = Accuracy(p1)
accp1.data
[3.0, 3.0, 4.0, 8.0, -9999, -9999, -9999, -9999, -9999]
\$\endgroup\$
4
  • \$\begingroup\$ Will you ever be calling the four functions ra_or, ra_os, etc. outside of Accuracy? \$\endgroup\$
    – unutbu
    Commented Mar 14, 2013 at 19:33
  • 1
    \$\begingroup\$ Are you looking for speed or convenience? \$\endgroup\$
    – unutbu
    Commented Mar 14, 2013 at 19:34
  • \$\begingroup\$ @unutbu both if it's possibile \$\endgroup\$
    – Gianni
    Commented Mar 14, 2013 at 19:37
  • \$\begingroup\$ @unutbu "ra_or, ra_os, etc. outside of Accuracy" not necessary. Do you think it's convenient call inside the class? \$\endgroup\$
    – Gianni
    Commented Mar 14, 2013 at 19:37

2 Answers 2

2
\$\begingroup\$

If you plan on calling the four functions ra_or, ra_os, sim_size and AFI outside of Accuracy then it is good to keep them as functions. If they never get called except through Accuracy, then they should be made methods.


Classes can help organize complex code, but they generally do not make your code faster. Do not use a class unless there is a clear advantage to be had -- through inheritance, or polymorphism, etc.

If you want faster code which uses less memory, avoid using a class here. Just define functions for each attribute.

If you want "luxurious" syntax -- the ability to reference each statistic via an attribute, then a class is fine.


If you plan on instantiating instances of Accuracy but not always accessing all the attributes, you don't need to compute them all in __init__. You can delay their computation by using properties.

 @property
 def area(self):
    return self.ref.area

Note that when you write accp1.area, the area method above will be called. Notice there are no parentheses after accp1.area.

To be clear, the advantage to using properties is that each instance of Accuracy will not compute all its statistical attributes until they are needed. The downside of using a property is that they are recomputed everytime the attribute is accessed. That may not be a downside if self.ref or self.seg ever change.

Moreover, you can cache the result using Denis Otkidach's CachedAttribute decorator. Then the attribute is only computed once, and simply looked up every time thereafter.


Don't use an arbitrary value for noData like noData = -9999. Use noData = np.nan, or simply skip noData and use np.nan directly.


import numpy as np
from shapely.geometry import Polygon
nan = np.nan

class Accuracy(object):
    def __init__(self, ref, seg=None):
        self.ref = ref
        self.seg = seg

    @property
    def area(self):
        return self.ref.area

    @property
    def perimeter(self):
        return self.ref.length

    @property
    def centroidX(self):
        return self.ref.centroid.x

    @property
    def centroidY(self):
        return self.ref.centroid.y

    @property
    def data(self):
        return [self.centroidX,
                self.centroidY,
                self.area,
                self.perimeter,
                self.segments,
                self.RAor,
                self.RAos,
                self.SimSize,
                self.AFI]

    @property
    def segments(self):
        if self.seg:
            return len(self.seg)
        else:
            return nan

    @property
    def RAor(self):
        if self.seg:
            return np.average(
                [(self.ref.intersection(s).area / self.ref.area) for s in self.seg])
        else:
            return nan

    @property
    def RAos(self):
        if self.seg:
            return np.average(
                [(self.ref.intersection(s).area / s.area) for s in self.seg])
        else:
            return nan

    @property
    def SimSize(self):
        if self.seg:
            return np.average(
                [(min(self.ref.area, s.area) / max(self.ref.area, s.area))
                 for s in self.seg])
        else:
            return nan

    @property
    def AFI(self):
        if self.seg:
            return (self.ref.area - max([s.area for s in self.seg])) / self.ref.area
        else:
            return nan

p1 = Polygon([(2, 4), (4, 4), (4, 2), (2, 2), (2, 4)])
p2 = Polygon([(0, 3), (3, 3), (3, 0), (0, 0), (0, 3)])

accp1 = Accuracy(p1, [p2])
print(accp1.data)
# [3.0, 3.0, 4.0, 8.0, 1, 0.25, 0.1111111111111111, 0.44444444444444442, -1.25]

accp1 = Accuracy(p1)
print(accp1.data)
# [3.0, 3.0, 4.0, 8.0, nan, nan, nan, nan, nan]

Here is how you could save your data (as a numpy array) to a CSV file:

np.savetxt('/tmp/mytest.txt', np.atleast_2d(accp1.data), delimiter=',')

And here is how you could read it back:

data = np.genfromtxt('/tmp/mytest.txt', dtype=None)
print(data)
# [  3.   3.   4.   8.  nan  nan  nan  nan  nan]
\$\endgroup\$
10
  • \$\begingroup\$ Thanks @unutbu, you are really good as usual. Just some explanation about noData. I wish to print on text file the result. For this reason i choose (just in code beta version) a value to print out and save in a text file. I am really interest in you opinion about this. \$\endgroup\$
    – Gianni
    Commented Mar 14, 2013 at 20:14
  • \$\begingroup\$ np.genfromtxt can parse csv files that use 'None', or 'NaN'. So you can use None or NaN just as easily as you could -9999. In fact, NaN may be a better choice since np.genfromtxt will make the column of dtype float, which is better for numerical work than the dtype of object that you would get from using None. \$\endgroup\$
    – unutbu
    Commented Mar 14, 2013 at 20:37
  • \$\begingroup\$ np.genfromtxt it's the first time i heard about this. Normally i use f = open("mytest.txt", "w") f.write(accp1.data) #fake f.close() \$\endgroup\$
    – Gianni
    Commented Mar 14, 2013 at 20:45
  • \$\begingroup\$ could you write two lines as example please \$\endgroup\$
    – Gianni
    Commented Mar 14, 2013 at 20:45
  • 1
    \$\begingroup\$ This is due to an unfortunate design decision in np.savetxt. When the array is 1-dimensional, the contents are printed one item per line. When the array is 2-dimensinoal, the contents are printed one row per line. You can get the 1-dimensional data on 1 line using `np.atleast_2d(...). See edit, above. \$\endgroup\$
    – unutbu
    Commented Mar 14, 2013 at 22:19
1
\$\begingroup\$

I would approach this class the same way that unutbu did, by just storing the polygons as attributes of the class, and using properties and methods for the analysis. I think the only thing I'd do differently would be to implement this as a subclass of Polygon, so any given instance could compare itself to any other polygon on demand. I'm not going to sketch out the details, but I would want it to work like this:

sp1 = SmartPolygon([(2, 4), (4, 4), (4, 2), (2, 2), (2, 4)])
sp2 = SmartPolygon([(0, 3), (3, 3), (3, 0), (0, 0), (0, 3)])

sp1.accuracy()
>>> (3.0, 3.0, 4.0, 8.0, nan, nan, nan, nan, nan)

sp1.accuracy(sp2)
>>> (3.0, 3.0, 4.0, 8.0, 1, 0.25, 0.1111111111111111, 0.44444444444444442, -1.25)

Other thoughts, in no particular order:


As its own class, I personally would use a different name. 'Accuracy' is an aspect of what you want to learn from the object, but not really representative of what the object is. I would call this class something like PolygonComparison, or something as descriptive but ideally more concise. I don't know if this Officially Pythonic or not, but I think of classes as nouns and functions/methods as verbs, and usually name them that way.


You have a few minor inconsistencies in your style, mainly by sometimes including spaces where you shouldn't or vice versa. These are generally things that won't affect how your code runs, but more how readable, understandable, and debuggable it is. For example,

def __init__ (self,ref,seg = None, noData = -9999):
# Note spacing:   ^   ^    ^              ^

would typically be written as

def __init__(self, ref, seg=None, noData=-9999):

Then it's much easier to see at a glance that the method has three parameters, two of which have defaults. I'd suggest taking a look at PEP 8, the Python style guide.


Finally, note that you have some redundancy in your __init__ method:

def __init__(self, ref, seg=None):
    if seg == None:
        self.area = ref.area
        self.perimeter = ref.length
        self.centroidX = ref.centroid.x
        self.centroidY = ref.centroid.y
        ... # etc.
    else:
        self.area = ref.area
        self.perimeter = ref.length
        self.centroidX = ref.centroid.x
        self.centroidY = ref.centroid.y
        ...

could be reduced to

def __init__(self, ref, seg=None):
    self.area = ref.area
    self.perimeter = ref.length
    self.centroidX = ref.centroid.x
    self.centroidY = ref.centroid.y

    # Typically you check if something *is* None, rather than *equals* None.
    if seg is None:
        ...
    else:
        ...
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.