├── demon ├── test │ ├── __init__.py │ └── demon_test.py ├── alg │ ├── __init__.py │ └── Demon.py ├── __init__.py └── __main__.py ├── requirements.txt ├── MANIFEST.in ├── conda ├── conda_build_config.yaml ├── build.sh └── meta.yaml ├── environment.yml ├── .coveragerc ├── setup.cfg ├── .travis.yml ├── .github └── workflows │ ├── publish.yml │ ├── python-package-conda.yml │ ├── build_pypi.yml │ └── test_ubuntu.yml ├── LICENSE ├── setup.py └── README.md /demon/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tqdm 2 | networkx>=2.4 3 | -------------------------------------------------------------------------------- /demon/alg/__init__.py: -------------------------------------------------------------------------------- 1 | from .Demon import Demon -------------------------------------------------------------------------------- /demon/__init__.py: -------------------------------------------------------------------------------- 1 | from demon.alg.Demon import Demon 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include requirements.txt 3 | include README.md -------------------------------------------------------------------------------- /conda/conda_build_config.yaml: -------------------------------------------------------------------------------- 1 | python: 2 | - 3.7 3 | - 3.8 4 | - 3.9 5 | -------------------------------------------------------------------------------- /demon/__main__.py: -------------------------------------------------------------------------------- 1 | from demon.alg.Demon import main 2 | 3 | main() 4 | 5 | -------------------------------------------------------------------------------- /conda/build.sh: -------------------------------------------------------------------------------- 1 | $PYTHON setup.py install --single-version-externally-managed --record=record.txt 2 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: demon_env 2 | dependencies: 3 | - python>=3.7 4 | - tqdm 5 | - networkx>=2.4 6 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | setup.py 4 | demon/__main__.py 5 | demon/test/* 6 | 7 | [report] 8 | exclude_lines = 9 | def main() -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.5" 4 | - "3.6" 5 | 6 | before_install: 7 | - pip install pytest pytest-cov 8 | - pip install coveralls 9 | 10 | install: 11 | - pip install . 12 | - pip install -r requirements.txt 13 | 14 | # command to run tests 15 | script: 16 | - py.test --cov=./ --cov-config=.coveragerc 17 | after_success: 18 | - coveralls 19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish_conda 2 | 3 | on: 4 | release: 5 | types: [published] 6 | #workflow_dispatch: # on demand 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: publish-to-conda 14 | uses: GiulioRossetti/conda-package-publish-action@v1.1.0 15 | with: 16 | subdir: 'conda' 17 | AnacondaToken: ${{ secrets.ANACONDA_TOKEN }} 18 | platforms: 'all' 19 | override: true 20 | # dry_run: true 21 | -------------------------------------------------------------------------------- /conda/meta.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: "demon" 3 | version: "2.0.6" 4 | 5 | source: 6 | git_rev: v2.0.6 7 | git_url: https://github.com/GiulioRossetti/DEMON 8 | 9 | requirements: 10 | host: 11 | - python 12 | - setuptools 13 | build: 14 | - python 15 | - setuptools 16 | run: 17 | - python>=3.7 18 | - tqdm 19 | - networkx>=2.4 20 | 21 | about: 22 | home: https://github.com/GiulioRossetti/DEMON 23 | license: BSD-2-Clause 24 | license_familY: BSD 25 | license_file: LICENSE 26 | summary: "DEMON - Overlapping Community Discovery" 27 | 28 | extra: 29 | recipe-maintainers: 30 | - GiulioRossetti 31 | -------------------------------------------------------------------------------- /demon/test/demon_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import demon as d 3 | import os 4 | import networkx as nx 5 | 6 | 7 | class DemonTestCase(unittest.TestCase): 8 | 9 | def test_demon_lib(self): 10 | g = nx.karate_club_graph() 11 | nx.write_edgelist(g, "test.csv", delimiter=" ") 12 | 13 | D = d.Demon(network_filename="test.csv", epsilon=0.3) 14 | coms = D.execute() 15 | print(coms) 16 | 17 | self.assertEqual(len(coms), 2) 18 | 19 | D = d.Demon(graph=g, file_output="communities.txt", epsilon=0.3) 20 | D.execute() 21 | 22 | f = open("communities.txt") 23 | count = 0 24 | for _ in f: 25 | count += 1 26 | self.assertEqual(count, 2) 27 | 28 | os.remove("test.csv") 29 | os.remove("communities.txt") 30 | 31 | if __name__ == '__main__': 32 | unittest.main() 33 | -------------------------------------------------------------------------------- /.github/workflows/python-package-conda.yml: -------------------------------------------------------------------------------- 1 | name: Python Package using Conda 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-linux: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | max-parallel: 5 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Python 3.8 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: 3.8 17 | - name: Add conda to system path 18 | run: | 19 | # $CONDA is an environment variable pointing to the root of the miniconda directory 20 | echo $CONDA/bin >> $GITHUB_PATH 21 | - name: Install dependencies 22 | run: | 23 | conda env update --file environment.yml --name base 24 | - name: Lint with flake8 25 | run: | 26 | conda install flake8 27 | # stop the build if there are Python syntax errors or undefined names 28 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 29 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 30 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 31 | - name: Test with pytest 32 | run: | 33 | conda install pytest 34 | pytest 35 | -------------------------------------------------------------------------------- /.github/workflows/build_pypi.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: pypi_packaging 10 | 11 | on: 12 | release: 13 | types: [published] 14 | #workflow_dispatch: 15 | 16 | jobs: 17 | deploy: 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: '3.x' 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install build 31 | 32 | - name: Build package 33 | run: python -m build 34 | 35 | - name: Publish distribution 📦 to PyPI 36 | if: startsWith(github.ref, 'refs/tags') 37 | uses: pypa/gh-action-pypi-publish@master 38 | with: 39 | user: __token__ 40 | password: ${{ secrets.PYPI_API_TOKEN }} 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Giulio Rossetti 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /.github/workflows/test_ubuntu.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Test and Coverage (Ubuntu) 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: [3.7, 3.8, 3.9] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: conda-incubator/setup-miniconda@v2 24 | with: 25 | auto-update-conda: true 26 | python-version: ${{ matrix.python-version }} 27 | 28 | 29 | - name: Install pip dependencies 30 | run: | 31 | 32 | python -m pip install --upgrade pip 33 | pip install . 34 | pip install -r requirements.txt 35 | python -m pip install flake8 pytest 36 | pip install pytest pytest-cov 37 | pip install coveralls 38 | 39 | - name: Lint with flake8 40 | run: | 41 | # stop the build if there are Python syntax errors or undefined names 42 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 43 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 44 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 45 | 46 | - name: Test with pytest 47 | run: | 48 | pytest --cov-config=.coveragerc --cov=./ --cov-report=xml 49 | 50 | - name: codecov 51 | uses: codecov/codecov-action@v1 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from codecs import open 3 | from os import path 4 | 5 | __author__ = 'Giulio Rossetti' 6 | __license__ = "BSD 2 Clause" 7 | __email__ = "giulio.rossetti@gmail.com" 8 | 9 | # Get the long description from the README file 10 | # with open(path.join(here, 'README.md'), encoding='utf-8') as f: 11 | # long_description = f.read() 12 | here = path.abspath(path.dirname(__file__)) 13 | 14 | # Get the long description from the README file 15 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 16 | long_description = f.read() 17 | 18 | with open(path.join(here, 'requirements.txt'), encoding='utf-8') as f: 19 | requirements = f.read().splitlines() 20 | 21 | setup(name='demon', 22 | version='2.0.6', 23 | license='BSD-2-Clause', 24 | description='Community Discovery algorithm', 25 | url='https://github.com/GiulioRossetti/DEMON', 26 | author='Giulio Rossetti', 27 | author_email='giulio.rossetti@gmail.com', 28 | use_2to3=True, 29 | classifiers=[ 30 | # How mature is this project? Common values are 31 | # 3 - Alpha 32 | # 4 - Beta 33 | # 5 - Production/Stable 34 | 'Development Status :: 5 - Production/Stable', 35 | 36 | # Indicate who your project is intended for 37 | 'Intended Audience :: Developers', 38 | 'Topic :: Software Development :: Build Tools', 39 | 40 | # Pick your license as you wish (should match "license" above) 41 | 'License :: OSI Approved :: BSD License', 42 | 43 | "Operating System :: OS Independent", 44 | 45 | # Specify the Python versions you support here. In particular, ensure 46 | # that you indicate whether you support Python 2, Python 3 or both. 47 | 'Programming Language :: Python', 48 | 'Programming Language :: Python :: 2.7', 49 | 'Programming Language :: Python :: 3' 50 | ], 51 | keywords=['complex-networks', 'community discovery'], 52 | install_requires=requirements,#['networkx>2.4', 'tqdm', ''], 53 | long_description=long_description, 54 | long_description_content_type='text/markdown', 55 | packages=find_packages(exclude=["*.test", "*.test.*", "test.*", "test", "demon.test", "demon.test.*"]), 56 | ) 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEMON - Overlapping Community Discovery. 2 | 3 | [![Test and Coverage (Ubuntu)](https://github.com/GiulioRossetti/DEMON/actions/workflows/test_ubuntu.yml/badge.svg)](https://github.com/GiulioRossetti/DEMON/actions/workflows/test_ubuntu.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/GiulioRossetti/DEMON/badge.svg?branch=master)](https://coveralls.io/github/GiulioRossetti/DEMON?branch=master) 5 | [![pyversions](https://img.shields.io/pypi/pyversions/demon.svg)](https://badge.fury.io/py/DEMON) 6 | [![PyPI version](https://badge.fury.io/py/demon.svg)](https://badge.fury.io/py/DEMON) 7 | [![Updates](https://pyup.io/repos/github/GiulioRossetti/DEMON/shield.svg)](https://pyup.io/repos/github/GiulioRossetti/DEMON/) 8 | [![DOI](https://zenodo.org/badge/53486170.svg)](https://zenodo.org/badge/latestdoi/53486170) 9 | [![PyPI download month](https://img.shields.io/pypi/dm/demon.svg?color=blue&style=plastic)](https://pypi.python.org/pypi/demon/) 10 | 11 | ![DEMON logo](http://www.giuliorossetti.net/about/wp-content/uploads/2013/07/Demon-300x233.png) 12 | 13 | 14 | Community discovery in complex networks is an interesting problem with a number of applications, especially in the knowledge extraction task in social and information networks. However, many large networks often lack a particular community organization at a global level. In these cases, traditional graph partitioning algorithms fail to let the latent knowledge embedded in modular structure emerge, because they impose a top-down global view of a network. We propose here a simple local-first approach to community discovery, able to unveil the modular organization of real complex networks. This is achieved by democratically letting each node vote for the communities it sees surrounding it in its limited view of the global system, i.e. its ego neighborhood, using a label propagation algorithm; finally, the local communities are merged into a global collection. 15 | 16 | **Note:** Demon has been integrated within [CDlib](http://cdlib.readthedocs.io) a python package dedicated to community detection algorithms, check it out! 17 | 18 | ## Citation 19 | If you use our algorithm please cite the following works: 20 | 21 | >Coscia, Michele; Rossetti, Giulio; Giannotti, Fosca; Pedreschi, Dino 22 | > ["Uncovering Hierarchical and Overlapping Communities with a Local-First Approach"](http://dl.acm.org/citation.cfm?id=2629511) 23 | >ACM Transactions on Knowledge Discovery from Data (TKDD), 9 (1), 2014. 24 | 25 | >Coscia, Michele; Rossetti, Giulio; Giannotti, Fosca; Pedreschi, Dino 26 | > ["DEMON: a Local-First Discovery Method for Overlapping Communities"](http://dl.acm.org/citation.cfm?id=2339630) 27 | >SIGKDD international conference on knowledge discovery and data mining, pp. 615-623, IEEE ACM, 2012, ISBN: 978-1-4503-1462-6. 28 | 29 | ## Installation 30 | 31 | 32 | In order to install the package just download (or clone) the current project and copy the demon folder in the root of your application. 33 | 34 | Alternatively use pip: 35 | ```bash 36 | pip install demon 37 | ``` 38 | 39 | or conda 40 | ```bash 41 | conda install -c giuliorossetti demon 42 | ``` 43 | 44 | Demon is written in python and requires the following package to run: 45 | - networkx 46 | - tqdm 47 | 48 | ## Implementation details 49 | 50 | 51 | 52 | # Execution 53 | 54 | The algorithm can be used as standalone program as well as integrated in python scripts. 55 | 56 | ## Standalone 57 | 58 | ```bash 59 | 60 | python demon filename epsilon -c min_com_size 61 | ``` 62 | 63 | where: 64 | * *filename*: edgelist filename 65 | * *epsilon*: merging threshold in [0,1] 66 | * *min_community_size*: minimum size for communities (default 3 - optional) 67 | 68 | Demon results will be saved on a text file. 69 | 70 | ### Input file specs 71 | Edgelist format: tab separated edgelist (nodes represented with integer ids). 72 | 73 | Row example: 74 | ``` 75 | node_id0 node_id1 76 | ``` 77 | 78 | ## As python library 79 | 80 | Demon can be executed specifying as input: 81 | 82 | 1. an edgelist file 83 | 84 | ```python 85 | import demon as d 86 | dm = d.Demon(network_filename="filename.tsc", epsilon=0.25, min_community_size=3, file_output="communities.txt") 87 | dm.execute() 88 | 89 | ``` 90 | 91 | 2. a *networkx* Graph object 92 | 93 | ```python 94 | import networkx as nx 95 | import demon as d 96 | 97 | g = nx.karate_club_graph() 98 | dm = d.Demon(graph=g, epsilon=0.25, min_community_size=3) 99 | coms = dm.execute() 100 | 101 | ``` 102 | 103 | The parameter *file_output*, if specified, allows to write on file the algorithm results. 104 | Conversely, the communities will be returned to the main program as a list of node ids tuple, e.g., 105 | 106 | ```python 107 | [(0,1,2),(3,4),(5,6,7)] 108 | ``` 109 | -------------------------------------------------------------------------------- /demon/alg/Demon.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import random 3 | import time 4 | import sys 5 | import tqdm 6 | import os 7 | 8 | __author__ = "Giulio Rossetti" 9 | __contact__ = "giulio.rossetti@isti.cnr.it" 10 | __license__ = "BSD 2 Clause" 11 | 12 | 13 | def timeit(method): 14 | """ 15 | Decorator: Compute the execution time of a function 16 | :param method: the function 17 | :return: the method runtime 18 | """ 19 | 20 | def timed(*arguments, **kw): 21 | ts = time.time() 22 | result = method(*arguments, **kw) 23 | te = time.time() 24 | 25 | sys.stdout.write('Time: %r %2.2f sec\n' % (method.__name__.strip("_"), te - ts)) 26 | sys.stdout.write('------------------------------------\n') 27 | sys.stdout.flush() 28 | return result 29 | 30 | return timed 31 | 32 | 33 | class Demon(object): 34 | """ 35 | Flat Merge version of Demon algorithm as described in: 36 | 37 | Michele Coscia, Giulio Rossetti, Fosca Giannotti, Dino Pedreschi: 38 | DEMON: a local-first discovery method for overlapping communities. 39 | KDD 2012:615-623 40 | """ 41 | 42 | def __init__(self, graph=None, network_filename=None, epsilon=0.25, min_community_size=3, file_output=None): 43 | """ 44 | Constructor 45 | 46 | :@param network_filename: the networkx filename 47 | :@param epsilon: the tolerance required in order to merge communities 48 | :@param min_community_size:min nodes needed to form a community 49 | :@param file_output: True/False 50 | """ 51 | if graph is None: 52 | self.g = nx.Graph() 53 | if network_filename is not None: 54 | self.__read_graph(network_filename) 55 | else: 56 | raise ImportError 57 | else: 58 | self.g = graph 59 | self.epsilon = epsilon 60 | self.min_community_size = min_community_size 61 | self.file_output = file_output 62 | self.base = os.getcwd() 63 | 64 | @timeit 65 | def __read_graph(self, network_filename): 66 | """ 67 | Read .ncol network file 68 | 69 | :param network_filename: complete path for the .ncol file 70 | :return: an undirected network 71 | """ 72 | self.g = nx.read_edgelist(network_filename, nodetype=int) 73 | 74 | @timeit 75 | def execute(self): 76 | """ 77 | Execute Demon algorithm 78 | 79 | """ 80 | 81 | for n in self.g.nodes(): 82 | self.g.nodes[n]['communities'] = [n] 83 | 84 | all_communities = {} 85 | 86 | for ego in tqdm.tqdm(nx.nodes(self.g), ncols=35, bar_format='Exec: {l_bar}{bar}'): 87 | 88 | ego_minus_ego = nx.ego_graph(self.g, ego, 1, False) 89 | community_to_nodes = self.__overlapping_label_propagation(ego_minus_ego, ego) 90 | 91 | # merging phase 92 | for c in community_to_nodes.keys(): 93 | if len(community_to_nodes[c]) > self.min_community_size: 94 | actual_community = community_to_nodes[c] 95 | all_communities = self.__merge_communities(all_communities, actual_community) 96 | 97 | # write output on file 98 | if self.file_output: 99 | with open(self.file_output, "w") as out_file_com: 100 | for idc, c in enumerate(all_communities.keys()): 101 | out_file_com.write("%d\t%s\n" % (idc, str(sorted(c)))) 102 | 103 | return list(all_communities.keys()) 104 | 105 | @staticmethod 106 | def __overlapping_label_propagation(ego_minus_ego, ego, max_iteration=10): 107 | """ 108 | 109 | :@param max_iteration: number of desired iteration for the label propagation 110 | :@param ego_minus_ego: ego network minus its center 111 | :@param ego: ego network center 112 | """ 113 | t = 0 114 | 115 | old_node_to_coms = {} 116 | 117 | while t <= max_iteration: 118 | t += 1 119 | 120 | node_to_coms = {} 121 | 122 | nodes = list(nx.nodes(ego_minus_ego)) 123 | random.shuffle(nodes) 124 | 125 | count = -len(nodes) 126 | 127 | for n in nodes: 128 | label_freq = {} 129 | 130 | n_neighbors = list(nx.neighbors(ego_minus_ego, n)) 131 | 132 | if len(n_neighbors) < 1: 133 | continue 134 | 135 | # compute the frequency of the labels 136 | for nn in n_neighbors: 137 | 138 | communities_nn = [nn] 139 | 140 | if nn in old_node_to_coms: 141 | communities_nn = old_node_to_coms[nn] 142 | 143 | for nn_c in communities_nn: 144 | if nn_c in label_freq: 145 | v = label_freq.get(nn_c) 146 | label_freq[nn_c] = v + 1 147 | else: 148 | label_freq[nn_c] = 1 149 | 150 | # first run, random community label initialization 151 | if t == 1: 152 | if not len(n_neighbors) == 0: 153 | r_label = random.sample(label_freq.keys(), 1) 154 | ego_minus_ego.nodes[n]['communities'] = r_label 155 | old_node_to_coms[n] = r_label 156 | count += 1 157 | continue 158 | 159 | # choosing the majority 160 | else: 161 | labels = [] 162 | max_freq = -1 163 | 164 | for l, c in label_freq.items(): 165 | if c > max_freq: 166 | max_freq = c 167 | labels = [l] 168 | elif c == max_freq: 169 | labels.append(l) 170 | 171 | node_to_coms[n] = labels 172 | 173 | if n not in old_node_to_coms or not set(node_to_coms[n]) == set(old_node_to_coms[n]): 174 | old_node_to_coms[n] = node_to_coms[n] 175 | ego_minus_ego.nodes[n]['communities'] = labels 176 | 177 | # build the communities reintroducing the ego 178 | community_to_nodes = {} 179 | for n in nx.nodes(ego_minus_ego): 180 | if len(list(nx.neighbors(ego_minus_ego, n))) == 0: 181 | ego_minus_ego.nodes[n]['communities'] = [n] 182 | 183 | c_n = ego_minus_ego.nodes[n]['communities'] 184 | 185 | for c in c_n: 186 | 187 | if c in community_to_nodes: 188 | com = community_to_nodes.get(c) 189 | com.append(n) 190 | else: 191 | nodes = [n, ego] 192 | community_to_nodes[c] = nodes 193 | 194 | return community_to_nodes 195 | 196 | def __merge_communities(self, communities, actual_community): 197 | """ 198 | 199 | :param communities: dictionary of communities 200 | :param actual_community: a community 201 | """ 202 | 203 | # if the community is already present return 204 | if tuple(actual_community) in communities: 205 | return communities 206 | 207 | else: 208 | # search a community to merge with 209 | inserted = False 210 | 211 | for test_community in communities.items(): 212 | 213 | union = self.__generalized_inclusion(actual_community, test_community[0]) 214 | 215 | # community to merge with identified! 216 | # N.B. one-to-one merge with no predefined visit ordering: non-deterministic behaviours expected 217 | if union is not None: 218 | communities.pop(test_community[0]) 219 | communities[tuple(sorted(union))] = 0 220 | inserted = True 221 | break 222 | 223 | # not merged: insert the original community 224 | if not inserted: 225 | communities[tuple(sorted(actual_community))] = 0 226 | 227 | return communities 228 | 229 | def __generalized_inclusion(self, c1, c2): 230 | """ 231 | 232 | :param c1: community 233 | :param c2: community 234 | """ 235 | intersection = set(c2) & set(c1) 236 | smaller_set = min(len(c1), len(c2)) 237 | 238 | if len(intersection) == 0: 239 | return None 240 | 241 | res = 0 242 | if not smaller_set == 0: 243 | res = float(len(intersection)) / float(smaller_set) 244 | 245 | if res >= self.epsilon: # at least e% of similarity wrt the smallest set 246 | union = set(c2) | set(c1) 247 | return union 248 | return None 249 | 250 | 251 | def main(): 252 | import argparse 253 | 254 | sys.stdout.write("-------------------------------------\n") 255 | sys.stdout.write(" {DEMON} \n") 256 | sys.stdout.write(" Democratic Estimate of the \n") 257 | sys.stdout.write(" Modular Organization of a Network \n") 258 | sys.stdout.write("-------------------------------------\n") 259 | sys.stdout.write("Author: " + __author__ + "\n") 260 | sys.stdout.write("Email: " + __contact__ + "\n") 261 | sys.stdout.write("------------------------------------\n") 262 | 263 | parser = argparse.ArgumentParser() 264 | 265 | parser.add_argument('network_file', type=str, help='network file (edge list format)') 266 | parser.add_argument('epsilon', type=float, help='merging threshold') 267 | parser.add_argument('-c', '--min_com_size', type=int, help='minimum community size', default=3) 268 | 269 | args = parser.parse_args() 270 | dm = Demon(g=None, network_filename=args.network_file, epsilon=args.epsilon, 271 | min_community_size=args.min_com_size, file_output="demon_communities.tsv") 272 | dm.execute() 273 | 274 | --------------------------------------------------------------------------------