Repository: networkx/networkx

Issue:
write_graphml does not recast properties with same name but different value types
```
g = nx.DiGraph()
g.add_node('a')
g.add_node('b')
g.node['a']['count'] = 1
g.node['b']['count'] = float(.5)
nx.write_graphml(g, "g.graphml")
```

The above code will result in two separate attribute keys within the XML file, one for the int and one for the float.

Expected operation would be for the 'count' property to be abstracted to the broadest type which could contain all values (in this case, float), and saved as a single attribute.



Discussion:
Good catch. I think we do a single-pass right now. This will require two passes but probably should be done.

If you wait to write the attribute type until the end, you can keep track of the property types encountered for a property and write the appropriate type to encompass them all at the end.

This could get tricky if the types are not so easily abstracted.  For example if someone has an attribute with  '1', 1, True ,and "True" as values, etc.  Maybe we shouldn't attempt to do something fancy here?

I def don't think we should be converting non-numeric types to numeric...and worrying about the numerous ways of representing bools is a pain. But numeric attributes seem like an easy use case to handle specially. 

I'd imagine the cases where one wants mixed numeric types is near nonexistant, and so if we see both int and float, there seems to be an obvious answer in terms of what the user would like during conversion...very similar to what most (friendly) CSV readers when a column contains numeric looking data but with only some having decimals in them. I guess we'd at least need to allow this inference to be be able to be disabled.

 boolean -> int -> long -> float -> double -> string.  Take the one most to the right?

@gdbassett, if there were an interface that allowed you to specify the type for each attribute, would that be sufficent? Or would you want it automatically inferred?

I think it should be inferred.  The simple nature of the write_\* functions is one of the benefits of the module.


Resolving patch:
diff --git a/networkx/readwrite/graphml.py b/networkx/readwrite/graphml.py
--- a/networkx/readwrite/graphml.py
+++ b/networkx/readwrite/graphml.py
@@ -41,6 +41,7 @@
 __all__ = ['write_graphml', 'read_graphml', 'generate_graphml',
            'parse_graphml', 'GraphMLWriter', 'GraphMLReader']
 
+from collections import defaultdict
 import networkx as nx
 from networkx.utils import open_file, make_str
 import warnings
@@ -53,13 +54,17 @@
         pass
 
 @open_file(1,mode='wb')
-def write_graphml(G, path, encoding='utf-8',prettyprint=True):
+def write_graphml(G, path, encoding='utf-8', prettyprint=True, infer_numeric_types=False):
     """Write G in GraphML XML format to path
 
     Parameters
     ----------
     G : graph
        A networkx graph
+    infer_numeric_types : boolean
+       Determine if numeric types should be generalized despite different python values.
+       For example, if edges have both int and float 'weight' attributes, it will be
+       inferred in GraphML that they are both floats (which translates to double in GraphML).
     path : file or string
        File or filename to write.
        Filenames ending in .gz or .bz2 will be compressed.
@@ -78,7 +83,7 @@ def write_graphml(G, path, encoding='utf-8',prettyprint=True):
     This implementation does not support mixed graphs (directed and unidirected
     edges together) hyperedges, nested graphs, or ports.
     """
-    writer = GraphMLWriter(encoding=encoding,prettyprint=prettyprint)
+    writer = GraphMLWriter(encoding=encoding,prettyprint=prettyprint,infer_numeric_types=infer_numeric_types)
     writer.add_graph_element(G)
     writer.dump(path)
 
@@ -230,12 +235,13 @@ class GraphML(object):
     }
 
 class GraphMLWriter(GraphML):
-    def __init__(self, graph=None, encoding="utf-8",prettyprint=True):
+    def __init__(self, graph=None, encoding="utf-8", prettyprint=True, infer_numeric_types=False):
         try:
             import xml.etree.ElementTree
         except ImportError:
              raise ImportError('GraphML writer requires '
                                'xml.elementtree.ElementTree')
+        self.infer_numeric_types = infer_numeric_types
         self.prettyprint=prettyprint
         self.encoding = encoding
         self.xml = Element("graphml",
@@ -244,6 +250,8 @@ def __init__(self, graph=None, encoding="utf-8",prettyprint=True):
                             'xsi:schemaLocation':self.SCHEMALOCATION}
                            )
         self.keys={}
+        self.attributes = defaultdict(list)
+        self.attribute_types = defaultdict(set)
 
         if graph is not None:
             self.add_graph_element(graph)
@@ -255,6 +263,37 @@ def __str__(self):
         s=tostring(self.xml).decode(self.encoding)
         return s
 
+    def attr_type(self, name, scope, value):
+        """Infer the attribute type of data named name. Currently this only
+        supports inference of numeric types.
+
+        If self.infer_numeric_types is false, type is used. Otherwise, pick the
+        most general of types found across all values with name and scope. This
+        means edges with data named 'weight' are treated separately from nodes
+        with data named 'weight'.
+        """
+        if self.infer_numeric_types:
+            types = self.attribute_types[(name, scope)]
+
+            try:
+                chr(12345)     # Fails on Py!=3.
+                long = int     # Py3K's int is our long type
+            except ValueError:
+                # Python 2.x
+                pass
+
+            if len(types) > 1:
+                if float in types:
+                    return float
+                elif long in types:
+                    return long
+                else:
+                    return int
+            else:
+                return list(types)[0]
+        else:
+            return type(value)
+
     def get_key(self, name, attr_type, scope, default):
         keys_key = (name, attr_type, scope)
         try:
@@ -292,13 +331,12 @@ def add_data(self, name, element_type, value,
         return data_element
 
     def add_attributes(self, scope, xml_obj, data, default):
-        """Appends attributes to edges or nodes.
+        """Appends attribute data to edges or nodes, and stores type information
+        to be added later. See add_graph_element.
         """
         for k,v in data.items():
-            default_value=default.get(k)
-            obj=self.add_data(make_str(k), type(v), make_str(v),
-                              scope=scope, default=default_value)
-            xml_obj.append(obj)
+            self.attribute_types[(make_str(k), scope)].add(type(v))
+            self.attributes[xml_obj].append([k, v, scope, default.get(k)])
 
     def add_nodes(self, G, graph_element):
         for node,data in G.nodes(data=True):
@@ -349,8 +387,19 @@ def add_graph_element(self, G):
         self.add_attributes("graph", graph_element, data, default)
         self.add_nodes(G,graph_element)
         self.add_edges(G,graph_element)
+
+        # self.attributes contains a mapping from XML Objects to a list of
+        # data that needs to be added to them.
+        # We postpone processing of this in order to do type inference/generalization.
+        # See self.attr_type
+        for (xml_obj, data) in self.attributes.items():
+            for (k, v, scope, default) in data:
+                xml_obj.append(self.add_data(make_str(k), self.attr_type(k, scope, v), make_str(v),
+                                             scope, default))
+
         self.xml.append(graph_element)
 
+
     def add_graphs(self, graph_list):
         """
         Add many graphs to this GraphML document.
@@ -526,10 +575,10 @@ def decode_data_elements(self, graphml_keys, obj_xml):
 
                 # check all the diffrent types of edges avaivable in yEd.
                 for e in ['PolyLineEdge', 'SplineEdge', 'QuadCurveEdge', 'BezierEdge', 'ArcEdge']:
-                	edge_label = data_element.find("{%s}%s/{%s}EdgeLabel"%
+                        edge_label = data_element.find("{%s}%s/{%s}EdgeLabel"%
                                                (self.NS_Y, e, (self.NS_Y)))
-                	if edge_label is not None:
-                		break
+                        if edge_label is not None:
+                                break
 
                 if edge_label is not None:
                     data['label'] = edge_label.text


Test patch:
diff --git a/networkx/readwrite/tests/test_graphml.py b/networkx/readwrite/tests/test_graphml.py
--- a/networkx/readwrite/tests/test_graphml.py
+++ b/networkx/readwrite/tests/test_graphml.py
@@ -121,6 +121,34 @@ def setUp(self):
         self.attribute_graph.add_edge('n5','n4',id='e6',weight=1.1)
         self.attribute_fh = io.BytesIO(self.attribute_data.encode('UTF-8'))
 
+        self.attribute_numeric_type_data = """<?xml version='1.0' encoding='utf-8'?>
+<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
+  <key attr.name="weight" attr.type="double" for="node" id="d1" />
+  <key attr.name="weight" attr.type="double" for="edge" id="d0" />
+  <graph edgedefault="directed">
+    <node id="n0">
+      <data key="d1">1</data>
+    </node>
+    <node id="n1">
+      <data key="d1">2.0</data>
+    </node>
+    <edge source="n0" target="n1">
+      <data key="d0">1</data>
+    </edge>
+    <edge source="n1" target="n1">
+      <data key="d0">1.0</data>
+    </edge>
+  </graph>
+</graphml>
+"""
+
+        self.attribute_numeric_type_graph = nx.DiGraph()
+        self.attribute_numeric_type_graph.add_node('n0', weight=1)
+        self.attribute_numeric_type_graph.add_node('n1', weight=2.0)
+        self.attribute_numeric_type_graph.add_edge('n0', 'n1', weight=1)
+        self.attribute_numeric_type_graph.add_edge('n1', 'n1', weight=1.0)
+        self.attribute_numeric_type_fh = io.BytesIO(self.attribute_numeric_type_data.encode('UTF-8'))
+
         self.simple_undirected_data="""<?xml version="1.0" encoding="UTF-8"?>
 <graphml xmlns="http://graphml.graphdrawing.org/xmlns"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
@@ -187,6 +215,33 @@ def test_read_simple_undirected_graphml(self):
             sorted(sorted(e) for e in G.edges()),
             sorted(sorted(e) for e in I.edges()))
 
+    def test_write_read_attribute_numeric_type_graphml(self):
+        from xml.etree.ElementTree import parse
+
+        G = self.attribute_numeric_type_graph
+        fh = io.BytesIO()
+        nx.write_graphml(G, fh, infer_numeric_types=True)
+        fh.seek(0)
+        H = nx.read_graphml(fh)
+        fh.seek(0)
+
+        assert_equal(sorted(G.nodes()), sorted(H.nodes()))
+        assert_equal(sorted(G.edges()), sorted(H.edges()))
+        assert_equal(sorted(G.edges(data=True)),
+                     sorted(H.edges(data=True)))
+        self.attribute_numeric_type_fh.seek(0)
+
+        xml = parse(fh)
+        # Children are the key elements, and the graph element
+        children = xml.getroot().getchildren()
+        assert_equal(len(children), 3)
+
+        keys = [child.items() for child in children[:2]]
+
+        assert_equal(len(keys), 2)
+        assert_in(('attr.type', 'double'), keys[0])
+        assert_in(('attr.type', 'double'), keys[1])
+
     def test_read_attribute_graphml(self):
         G=self.attribute_graph
         H=nx.read_graphml(self.attribute_fh)
@@ -442,4 +497,3 @@ def test_bool(self):
         H=nx.parse_graphml(s)
         assert_equal(H.node['n0']['test'],True)
         assert_equal(H.node['n2']['test'],False)
-