[
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: pip\n  directory: \"/\"\n  schedule:\n    interval: daily\n  open-pull-requests-limit: 10\n"
  },
  {
    "path": ".github/workflows/coverage.yml",
    "content": "name: Calculate test coverage\n\non: [push]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Check out\n      uses: actions/checkout@v2\n    - name: Set up Python\n      uses: actions/setup-python@v2\n      with:\n        python-version: \"3.12\"\n    - uses: actions/cache@v2\n      name: Configure pip caching\n      with:\n        path: ~/.cache/pip\n        key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}\n        restore-keys: |\n          ${{ runner.os }}-pip-\n    - name: Install Python dependencies\n      run: |\n        pip install --upgrade pip\n        pip install -e .[community]\n        pip install pytest pytest-cov\n    - name: Run tests\n      run: |-\n        pytest --cov=linkpred --cov-report xml:coverage.xml --cov-report term\n    - name: Upload coverage report\n      uses: codecov/codecov-action@v1\n      with:\n        token: ${{ secrets.CODECOV_TOKEN }}\n        file: coverage.xml\n"
  },
  {
    "path": ".github/workflows/publish-to-pypi.yml",
    "content": "name: Publish 📦 to PyPI\non: push\n\njobs:\n  build:\n    name: Build distribution 📦\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n    - name: Set up Python\n      uses: actions/setup-python@v4\n      with:\n        python-version: \"3.x\"\n    - name: Install pypa/build\n      run: >-\n        python3 -m\n        pip install\n        build\n        --user\n    - name: Build a binary wheel and a source tarball\n      run: python3 -m build\n    - name: Store the distribution packages\n      uses: actions/upload-artifact@v3\n      with:\n        name: python-package-distributions\n        path: dist/\n\n  publish-to-pypi:\n    name: >-\n      Publish Python 🐍 distribution 📦 to PyPI\n    if: startsWith(github.ref, 'refs/tags/')  # only publish to PyPI on tag pushes\n    needs:\n    - build\n    runs-on: ubuntu-latest\n    environment:\n      name: pypi\n      url: https://pypi.org/p/linkpred\n    permissions:\n      id-token: write  # IMPORTANT: mandatory for trusted publishing\n\n    steps:\n    - name: Download all the dists\n      uses: actions/download-artifact@v3\n      with:\n        name: python-package-distributions\n        path: dist/\n    - name: Publish distribution 📦 to PyPI\n      uses: pypa/gh-action-pypi-publish@release/v1\n\n  github-release:\n    name: >-\n      Sign the Python 🐍 distribution 📦 with Sigstore\n      and upload them to GitHub Release\n    needs:\n    - publish-to-pypi\n    runs-on: ubuntu-latest\n\n    permissions:\n      contents: write  # IMPORTANT: mandatory for making GitHub Releases\n      id-token: write  # IMPORTANT: mandatory for sigstore\n\n    steps:\n    - name: Download all the dists\n      uses: actions/download-artifact@v4\n      with:\n        name: python-package-distributions\n        path: dist/\n    - name: Sign the dists with Sigstore\n      uses: sigstore/gh-action-sigstore-python@v3.0.0\n      with:\n        inputs: >-\n          ./dist/*.tar.gz\n          ./dist/*.whl\n    - name: Create GitHub Release\n      env:\n        GITHUB_TOKEN: ${{ github.token }}\n      run: >-\n        gh release create\n        '${{ github.ref_name }}'\n        --repo '${{ github.repository }}'\n        --notes \"\"\n    - name: Upload artifact signatures to GitHub Release\n      env:\n        GITHUB_TOKEN: ${{ github.token }}\n      # Upload to GitHub Release using the `gh` CLI.\n      # `dist/` contains the built packages, and the\n      # sigstore-produced signatures and certificates.\n      run: >-\n        gh release upload\n        '${{ github.ref_name }}' dist/**\n        --repo '${{ github.repository }}'\n"
  },
  {
    "path": ".github/workflows/tox.yml",
    "content": "name: Test with tox\n\non: [push]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      max-parallel: 5\n      matrix:\n        python-version: [\"3.8\", \"3.9\", \"3.10\", \"3.11\", \"3.12\"]\n\n    steps:\n    - uses: actions/checkout@v2\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v2\n      with:\n        python-version: ${{ matrix.python-version }}\n    - uses: actions/cache@v2\n      name: Configure pip caching\n      with:\n        path: ~/.cache/pip\n        key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}\n        restore-keys: |\n          ${{ runner.os }}-pip-\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install tox tox-gh-actions\n    - name: Test with tox\n      run: tox\n"
  },
  {
    "path": ".gitignore",
    "content": "*.py[cod]\n*.swp\n\nexamples/*.pdf\nexamples/*.png\nexamples/*.txt\n\n.ropeproject\n\n# C extensions\n*.so\n\n# Packages\n*.egg\n*.egg-info\ndist\nbuild\neggs\nparts\nbin\nvar\nsdist\ndevelop-eggs\n.installed.cfg\nlib\nlib64\n\n# Installer logs\npip-log.txt\n\n# Unit test / coverage reports\n.coverage\n.tox\nnosetests.xml\n\n# Translations\n*.mo\n\n# IDEs\n.mr.developer.cfg\n.project\n.pydevproject\n.idea\n.vscode\n\n.mypy_cache\n.cache\n"
  },
  {
    "path": "CHANGELOG.rst",
    "content": "Changelog\n=========\n\n**Note**: I only started keeping this changelog from version 0.5 onwards.\n\nVersion 0.6\n-----------\n\n- Officially support Python versions 3.8-3.12\n\n- Fix bug where linkpred could no longer be installed on Windows\n\n- General modernization of code and especially infrastructure (CI, packaging etc.)\n\nVersion 0.5.1\n-------------\n\n(I botched the release of v0.5, hence 0.5.1)\n\n- Python 3.8 officially supported!\n\n- Behind-the-scenes work: testing is now done with pytest and tox, formatting is done by black, and we are based on the latest version of networkx (2.4).\n\n- The Community predictor is now easier to use, also because its optional dependency (`python-louvain <https://github.com/taynaud/python-louvain>`_) is now on PyPI. If you want to use it, install as follows:\n\n    $ pip install linkpred[all]\n\n- Some bug fixes.\n"
  },
  {
    "path": "LICENSE",
    "content": "New BSD License\n\nCopyright (c) 2013 The linkpred developers.\nAll rights reserved.\n\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n  a. Redistributions of source code must retain the above copyright notice,\n     this list of conditions and the following disclaimer.\n  b. Redistributions in binary form must reproduce the above copyright\n     notice, this list of conditions and the following disclaimer in the\n     documentation and/or other materials provided with the distribution.\n  c. Neither the name of the linkpred developers nor the names of\n     its contributors may be used to endorse or promote products\n     derived from this software without specific prior written\n     permission.\n\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\nLIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY\nOUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGE.\n\n\n"
  },
  {
    "path": "README.rst",
    "content": "⚠️ **Note: This package is in maintenance mode**.\nCritical bugs will continue to be resolved,\nbut no new features will be implemented (`more information <https://github.com/rafguns/linkpred/issues/35>`_).\n\nLinkpred\n========\n\n**Linkpred** is a Python package for link prediction: given a network, Linkpred provides a number of heuristics (known as *predictors*) that assess the likelihood of potential links in a future snapshot of the network.\n\nWhile some predictors are fairly straightforward (e.g., if two people have a large number of mutual friends, it seems likely that eventually they will meet and become friends), others are more involved.\n\n.. image:: https://codecov.io/gh/rafguns/linkpred/branch/master/graph/badge.svg?token=JVZIVHWJXY \n   :target: https://codecov.io/gh/rafguns/linkpred\n\n**linkpred** can both be used as a command-line tool and as a Python library in your own code.\n\n\nInstallation\n------------\n\n**linkpred** (v0.6 and later) works under Python 3.8 to 3.12.\nIt depends on:\n\n- matplotlib\n- networkx\n- numpy\n- pyyaml\n- scipy\n- smokesignal\n\nYou should be able to install Linkpred and its dependencies using pip (``pip install linkpred`` or ``python -m pip install linkpred``).\nIf you do not yet have Python installed, I recommend starting with `Anaconda <https://www.continuum.io/downloads>`_,\nwhich includes optimized versions of packages like numpy.\nIf you want to use the Community predictor, which relies on community structure of the network,\nmake sure you also have the `python-louvain <https://github.com/taynaud/python-louvain>`_ package by installing with ``pip install linkpred[community]``.\n\n\nExample usage as command-line tool\n----------------------------------\n\nA good starting point is ``linkpred --help``, which lists all the available options. To save the predictions of the ``CommonNeighbours`` predictor, for instance, run::\n\n    $ linkpred examples/inf1990-2004.net -p CommonNeighbours --output cache-predictions\n\nwhere ``examples/inf1990-2004.net`` is a network file in Pajek format. Other supported formats include GML and GraphML. The full output looks like this:\n\n.. code:: console\n\n    $ linkpred examples/inf1990-2004.net -p CommonNeighbours --output cache-predictions\n    16:43:13 - INFO - Reading file 'examples/inf1990-2004.net'...\n    16:43:13 - INFO - Successfully read file.\n    16:43:13 - INFO - Starting preprocessing...\n    16:43:13 - INFO - Removed 35 nodes (degree < 1)\n    16:43:13 - INFO - Finished preprocessing.\n    16:43:13 - INFO - Executing CommonNeighbours...\n    16:43:14 - INFO - Finished executing CommonNeighbours.\n    16:43:14 - INFO - Prediction run finished\n\n    $ head examples/inf1990-2004-CommonNeighbours-predictions_2016-04-22_16.43.txt\n    \"Ikogami, K\"    \"Ikegami, K\"    5.0\n    \"Durand, T\"     \"Abd El Kader, M\"       5.0\n    \"Sharma, L\"     \"Kumar, S\"      4.0\n    \"Paul, A\"       \"Durand, T\"     4.0\n    \"Paul, A\"       \"Dudognon, G\"   4.0\n    \"Paul, A\"       \"Abd El Kader, M\"       4.0\n    \"Karisiddippa, CR\"      \"Garg, KC\"      4.0\n    \"Wu, YS\"        \"Kretschmer, H\" 3.0\n    \"Veugelers, R\"  \"Deleus, F\"     3.0\n    \"Veugelers, R\"  \"Andries, P\"    3.0\n\n\nExample usage within Python\n---------------------------\n\n.. code:: pycon\n\n    >>> import linkpred\n    >>> G = linkpred.read_network(\"examples/training.net\")\n    11:49:00 - INFO - Reading file 'examples/training.net'...\n    11:49:00 - INFO - Successfully read file.\n    >>> len(G)   # number of nodes\n    632\n    >>> # We exclude edges already present, to predict only new links\n    >>> simrank = linkpred.predictors.SimRank(G, excluded=G.edges())\n    >>> simrank_results = simrank.predict(c=0.5)\n    >>> top = simrank_results.top(5)\n    >>> for authors, score in top.items():\n    ...    print(authors, score)\n    ...\n    Tomizawa, H - Fujigaki, Y 0.188686630053\n    Shirabe, M - Hayashi, T 0.143866427916\n    Garfield, E - Fuseler, EA 0.148097050146\n    Persson, O - Larsen, IM 0.138516589957\n    Vanleeuwen, TN - Noyons, ECM 0.185040358711\n"
  },
  {
    "path": "examples/inf1990-2004.net",
    "content": "*network Informetrics1990-2004\n*vertices 632\n1 \"Pereira, JCR\"\n2 \"Peters, HPF\"\n3 \"Widhalm, C\"\n4 \"Verbeek, A\"\n5 \"Salvador, P\"\n6 \"Van Den Besselaar, P\"\n7 \"Koschatzky, K\"\n8 \"Sreenivas, V\"\n9 \"Dos Santos, NF\"\n10 \"He, L\"\n11 \"Lannes, D\"\n12 \"Yuthavong, Y\"\n13 \"Pant, N\"\n14 \"Salman, S\"\n15 \"Van Looy, B\"\n16 \"Hood, WW\"\n17 \"Garcia-zorita, C\"\n18 \"Green, L\"\n19 \"Bonzi, S\"\n20 \"Zitt, M\"\n21 \"Pereira, TS\"\n22 \"Vargas-quesada, B\"\n23 \"Dore, JC\"\n24 \"Rivaud, D\"\n25 \"Clarysse, B\"\n26 \"Cherny, AI\"\n27 \"Wright, B\"\n28 \"Mcmillan, GS\"\n29 \"Wu, Z\"\n30 \"Li, J4\"\n31 \"Mckenzie, G\"\n32 \"Lihua, L\"\n33 \"Chen, DQ\"\n34 \"Schneider, JW\"\n35 \"Zhu, XY\"\n36 \"Legentil, M\"\n37 \"Gurjeva, LG\"\n38 \"Warner, J\"\n39 \"Arora, J\"\n40 \"Elalami, J\"\n41 \"Wilkinson, D\"\n42 \"Yemenu, D\"\n43 \"De Los Santos-riog, M\"\n44 \"Bauin, S\"\n45 \"Agudelo, D\"\n46 \"Watts, RJ\"\n47 \"Havemann, F\"\n48 \"Thijs, B\"\n49 \"Rao, IKR\"\n50 \"Lemarie, J\"\n51 \"Dillon, M\"\n52 \"Mccain, KW\"\n53 \"Kovacs-nemeth, E\"\n54 \"Shama, G\"\n55 \"Dawson, G\"\n56 \"Kundra, R\"\n57 \"Callon, M\"\n58 \"Maciaschapula, CA\"\n59 \"Kandhari, R\"\n60 \"Viedma, MI\"\n61 \"Milisproost, G\"\n62 \"Chaimovich, H\"\n63 \"Shlesinger, MF\"\n64 \"Fenton, MR\"\n65 \"Kessler, C\"\n66 \"Mahlck, P\"\n67 \"Leemans, MJ\"\n68 \"Oppenheim, C\"\n69 \"Luukkonen, T\"\n70 \"Bocock, D\"\n71 \"Scaller, RR\"\n72 \"Weingart, P\"\n73 \"Eisemon, TO\"\n74 \"Ahmed, T\"\n75 \"Weber, M\"\n76 \"Vaughan, L\"\n77 \"Cunningham, S\"\n78 \"Cunningham, P\"\n79 \"Ren, SL\"\n80 \"Prime, C\"\n81 \"Butler, L\"\n82 \"Deyong, C\"\n83 \"Fujigaki, Y\"\n84 \"Banerjee, P\"\n85 \"Fuseler, EA\"\n86 \"Navarro, A\"\n87 \"Humenik, JA\"\n88 \"Rey, J\"\n89 \"Barrigon, S\"\n90 \"Stanard, C\"\n91 \"Kim, MS\"\n92 \"Lapid, K\"\n93 \"Heimeriks, G\"\n94 \"Fawcett, G\"\n95 \"Contreras, EJ\"\n96 \"Nazer, N\"\n97 \"Binns, R\"\n98 \"Frigoletto, L\"\n99 \"Martin-sempere, MJ\"\n100 \"Melin, G\"\n101 \"Galvez, L\"\n102 \"Clement, F\"\n103 \"Collazo-reyes, F\"\n104 \"Engwall, L\"\n105 \"Pudovkin, AI\"\n106 \"Garciajover, F\"\n107 \"Price, L\"\n108 \"Szabadi-peresztegi, Z\"\n109 \"Johnson, B\"\n110 \"Etemad, S\"\n111 \"Davis, CH\"\n112 \"Moed, H\"\n113 \"Buter, RK\"\n114 \"Li, XM\"\n115 \"Markusova, VA\"\n116 \"Kuhlmann, S\"\n117 \"Libkind, AN\"\n118 \"Deleus, F\"\n119 \"Uehara, M\"\n120 \"Debackere, K\"\n121 \"Sivertsen, G\"\n122 \"Yu, DR\"\n123 \"Solari, A\"\n124 \"Buela-casal, G\"\n125 \"Reedijk, J\"\n126 \"Wolfram, D\"\n127 \"Saavedra, F\"\n128 \"Dastidar, PG\"\n129 \"Niemi, T\"\n130 \"Cheng, YR\"\n131 \"Feillet, H\"\n132 \"Wofchuk, S\"\n133 \"Van Raan, AFJ\"\n134 \"Boerner, K\"\n135 \"Eberhart, HJ\"\n136 \"Munoz, E\"\n137 \"Mackenzie, MR\"\n138 \"Lacasa, ID\"\n139 \"Maczelka, H\"\n140 \"Glaser, J\"\n141 \"Davis, M\"\n142 \"Meyer, M\"\n143 \"Abd El Kader, M\"\n144 \"Ugena, S\"\n145 \"Lemarie, S\"\n146 \"Klavans, R\"\n147 \"Guo, YZ\"\n148 \"Garzon-garcia, B\"\n149 \"Persson, O\"\n150 \"Schwarz, S\"\n151 \"Claveria, LE\"\n152 \"Ricoy, JR\"\n153 \"Zhang, JG\"\n154 \"Bobb, K\"\n155 \"De Francisco, A\"\n156 \"Lee, YS\"\n157 \"Whitlow, ES\"\n158 \"Hamilton, RD\"\n159 \"Yagi, E\"\n160 \"Linstone, HA\"\n161 \"Wouters, P\"\n162 \"White, HD\"\n163 \"Bonitz, M\"\n164 \"Weaverwozniak, S\"\n165 \"Rey-rocha, J\"\n166 \"Snyder, H\"\n167 \"Ikogami, K\"\n168 \"Ojasoo, T\"\n169 \"Ebeling, W\"\n170 \"Ikegami, K\"\n171 \"Godoy, V\"\n172 \"Harries, G\"\n173 \"Cronin, B\"\n174 \"Archambault, E\"\n175 \"Garg, KC\"\n176 \"Griffith, BC\"\n177 \"Sanz, E\"\n178 \"Chiu, WT\"\n179 \"Larsen, B\"\n180 \"Bar-ilan, J\"\n181 \"Higashi, T\"\n182 \"Narin, F\"\n183 \"Courtial, JP\"\n184 \"Schweighoffer, MG\"\n185 \"Del Castillo, JM\"\n186 \"Breitzman, T\"\n187 \"Lascurain-sanchez, ML\"\n188 \"Grant, J\"\n189 \"Fairclough, R\"\n190 \"Jepsen, ET\"\n191 \"Maes, M\"\n192 \"Gaillard, J\"\n193 \"Magde, B\"\n194 \"Park, YT\"\n195 \"Munoz-fernandez, FJ\"\n196 \"Mello, J\"\n197 \"Muller, R\"\n198 \"Bjorneborn, L\"\n199 \"Neelameghan, A\"\n200 \"Clausen, H\"\n201 \"Lipworth, S\"\n202 \"Gourdon, L\"\n203 \"Viby-mogensen, J\"\n204 \"Brocken, M\"\n205 \"Peritz, BC\"\n206 \"Gaudy, JF\"\n207 \"Mason, B\"\n208 \"Fang, Y\"\n209 \"Meertens, RW\"\n210 \"Braun, T\"\n211 \"Paraje, G\"\n212 \"Danell, R\"\n213 \"Zulueta, MA\"\n214 \"Noyons, ECM\"\n215 \"Vermeulin, P\"\n216 \"Ortega, JL\"\n217 \"Arvanitis, R\"\n218 \"Ramanana-rahary, S\"\n219 \"Zimmermann, E\"\n220 \"Aggarwal, BS\"\n221 \"Geisler, E\"\n222 \"Robert, C\"\n223 \"Suarez-balseiro, CA\"\n224 \"Scharnhorst, A\"\n225 \"Granstrand, O\"\n226 \"Durand, T\"\n227 \"Thirion, B\"\n228 \"Okubo, Y\"\n229 \"Caridad, IG\"\n230 \"Liang, LM\"\n231 \"Small, H\"\n232 \"Delgado, H\"\n233 \"Narvaezberthelemot, N\"\n234 \"Larsen, IM\"\n235 \"Igic, R\"\n236 \"Sotolongo-aguilar, GR\"\n237 \"Satyanarayana, K\"\n238 \"Almind, TC\"\n239 \"De Moya-anegon, F\"\n240 \"Tijssen, R\"\n241 \"Skram, U\"\n242 \"Callahan, E\"\n243 \"Jagodzinskisigogneau, M\"\n244 \"Bruckner, E\"\n245 \"Okubo, T\"\n246 \"Martin, J\"\n247 \"Chinchilla-rodriguez, Z\"\n248 \"Zsindely, S\"\n249 \"Martin, A\"\n250 \"Mutafov, HG\"\n251 \"Poca, MA\"\n252 \"Rodrigues, PS\"\n253 \"Rubio, L\"\n254 \"Gherbi, R\"\n255 \"Hesselink, FT\"\n256 \"Rubio, E\"\n257 \"Stevens, KA\"\n258 \"Meyer, JB\"\n259 \"Detampel, MJ\"\n260 \"Debruin, RE\"\n261 \"Seiden, P\"\n262 \"Levy, D\"\n263 \"Schmoch, U\"\n264 \"Velloso, A\"\n265 \"Iversen, EJ\"\n266 \"Downie, JS\"\n267 \"Delgado-lopez-cozar, E\"\n268 \"Wang, L1\"\n269 \"Cluzeau, F\"\n270 \"Rosas, AM\"\n271 \"Pastor, A\"\n272 \"Badash, L\"\n273 \"Itoh, T\"\n274 \"Matthiessen, CW\"\n275 \"Kostoff, RN\"\n276 \"Karisiddippa, CR\"\n277 \"Leydesdorff, L\"\n278 \"Hicks, D\"\n279 \"Azerad, J\"\n280 \"Jarvelin, K\"\n281 \"Deroulede, A\"\n282 \"Fonseca, L\"\n283 \"Rumjanek, VM\"\n284 \"Dutt, B\"\n285 \"Waast, R\"\n286 \"Figueira, I\"\n287 \"Bourke, P\"\n288 \"Agis, A\"\n289 \"Hellgardt, K\"\n290 \"Kongthon, A\"\n291 \"Topolnik, M\"\n292 \"Sinilainen, T\"\n293 \"Laville, F\"\n294 \"Sharma, SC\"\n295 \"Vanhooydonk, G\"\n296 \"Traynor, M\"\n297 \"Medina, A\"\n298 \"Van Vuren, HG\"\n299 \"Smith, AG\"\n300 \"Czerwon, HJ\"\n301 \"Gupta, BM\"\n302 \"Kobayashi, S\"\n303 \"Fawcettjones, A\"\n304 \"Sigogneau, A\"\n305 \"Garfield, E\"\n306 \"Tomizawa, H\"\n307 \"Veugelers, R\"\n308 \"Leta, J\"\n309 \"Ho, YS\"\n310 \"Bruil, J\"\n311 \"Todorov, R\"\n312 \"Coronini, R\"\n313 \"Rodriguez-farre, E\"\n314 \"Kretschmer, H\"\n315 \"Pennebaker, JW\"\n316 \"Machado, RDP\"\n317 \"Demarco, RA\"\n318 \"Wormell, I\"\n319 \"Russell, JM\"\n320 \"Georgel, A\"\n321 \"Wu, YS\"\n322 \"Minin, VA\"\n323 \"Jacques, R\"\n324 \"Charum, J\"\n325 \"Martinson, A\"\n326 \"Sauquillo, J\"\n327 \"Lewison, G\"\n328 \"Rong, YH\"\n329 \"Park, HW\"\n330 \"Jacobs, D\"\n331 \"Devey, ME\"\n332 \"Rowlands, I\"\n333 \"Engelsman, EC\"\n334 \"Xu, HD\"\n335 \"Morillo, F\"\n336 \"Visser, MS\"\n337 \"Pal, C\"\n338 \"Korevaar, JC\"\n339 \"Zhang, YH\"\n340 \"Pistorius, C\"\n341 \"Tanaka, C\"\n342 \"Limoges, C\"\n343 \"Oneill, E\"\n344 \"Shan, S\"\n345 \"Seglen, PO\"\n346 \"Gevaert, R\"\n347 \"Del Rio, JA\"\n348 \"Reinert, M\"\n349 \"Meijer, RF\"\n350 \"Yue, WP\"\n351 \"Senkovska, ED\"\n352 \"Grupp, H\"\n353 \"Corera-alvarez, E\"\n354 \"Torres, G\"\n355 \"Kyvik, S\"\n356 \"Pastor, R\"\n357 \"Zhang, QQ\"\n358 \"Karki, MMS\"\n359 \"Paul, A\"\n360 \"Cottrell, R\"\n361 \"Zelman, A\"\n362 \"Katz, JS\"\n363 \"Yang, ZQ\"\n364 \"Cothey, V\"\n365 \"Wu, GZ\"\n366 \"Pearson, S\"\n367 \"Benichou, J\"\n368 \"Niwa, F\"\n369 \"Carretero-dios, H\"\n370 \"Osareh, F\"\n371 \"Jain, A\"\n372 \"Ma, Z\"\n373 \"Berg, J\"\n374 \"Urdin, MC\"\n375 \"Caldeira, MT\"\n376 \"Ajiferuke, I\"\n377 \"Bordons, M\"\n378 \"Rojouan, F\"\n379 \"Andries, P\"\n380 \"Losiewicz, P\"\n381 \"Gilyarevskii, RS\"\n382 \"Gilbert, J\"\n383 \"Pellenbarg, R\"\n384 \"Schubert, GA\"\n385 \"Borlund, P\"\n386 \"Malpohl, G\"\n387 \"Wagnerdobler, R\"\n388 \"Toothman, DR\"\n389 \"Bravo, C\"\n390 \"Devillard, J\"\n391 \"Huber, JC\"\n392 \"Wilson, CS\"\n393 \"Burrell, QL\"\n394 \"Van Der Wurff, LJ\"\n395 \"He, DG\"\n396 \"Tomov, DT\"\n397 \"Karypis, G\"\n398 \"Schoepflin, U\"\n399 \"Roussel, F\"\n400 \"Barnett, GA\"\n401 \"Hayashi, T\"\n402 \"Zanetta, DMT\"\n403 \"Rodea-castro, IP\"\n404 \"Torricella-morales, RG\"\n405 \"Davidse, RJ\"\n406 \"Van Den Berghe, H\"\n407 \"Rafferty, AM\"\n408 \"Jiang, L\"\n409 \"Gauthier, L\"\n410 \"Delooze, MA\"\n411 \"Aksnes, DW\"\n412 \"Moreno, L\"\n413 \"Welljamsdorof, A\"\n414 \"Jacquemin, C\"\n415 \"Hirvonen, L\"\n416 \"Page-kennedy, T\"\n417 \"Haritash, N\"\n418 \"Pessot, R\"\n419 \"Petard, JP\"\n420 \"Bookstein, A\"\n421 \"Horlesberger, M\"\n422 \"Sehringer, R\"\n423 \"Shaw, D\"\n424 \"Cahlik, T\"\n425 \"Van Hulle, MM\"\n426 \"Bogaert, J\"\n427 \"Winterhager, M\"\n428 \"Lozano, S\"\n429 \"Dudognon, G\"\n430 \"Wilke, HAM\"\n431 \"Heydari, A\"\n432 \"Musgrove, PB\"\n433 \"Guallar, E\"\n434 \"Zhao, HZ\"\n435 \"Matillon, Y\"\n436 \"Carding, P\"\n437 \"Martin-moreno, C\"\n438 \"Kim, CS\"\n439 \"Mangematin, V\"\n440 \"Arikan, F\"\n441 \"Herrero-solana, V\"\n442 \"Gilmour, JE\"\n443 \"Ranga, LM\"\n444 \"Wellman, B\"\n445 \"Chatelin, Y\"\n446 \"Sarbolouki, MN\"\n447 \"Lelu, A\"\n448 \"Lustosa, P\"\n449 \"Rousseau, R\"\n450 \"Rousseau, S\"\n451 \"Bernal, G\"\n452 \"Harsanyi, MA\"\n453 \"Prat, AM\"\n454 \"Pan, YT\"\n455 \"Schlemmer, B\"\n456 \"Yamazaki, S\"\n457 \"Lemarc, M\"\n458 \"Garcia, EO\"\n459 \"Rippon, I\"\n460 \"Jansen, P\"\n461 \"Delange, C\"\n462 \"Albertini, R\"\n463 \"Hsieh, WH\"\n464 \"Krauskopf, V\"\n465 \"Lakshmi, VV\"\n466 \"Van Vijk, E\"\n467 \"Schiebel, E\"\n468 \"Yitzhaki, M\"\n469 \"Wang, B\"\n470 \"Boyack, KW\"\n471 \"Kuntze, U\"\n472 \"Ruiz-perez, R\"\n473 \"Beckmann, M\"\n474 \"Araujo-ruiz, JA\"\n475 \"Kaloudis, A\"\n476 \"Wang, Y\"\n477 \"Srivastava, D\"\n478 \"Olsen, TB\"\n479 \"Cabrero, A\"\n480 \"Hamilton, KS\"\n481 \"Jin, BH\"\n482 \"Jeannin, P\"\n483 \"Zhang, HQ\"\n484 \"Teixeira, N\"\n485 \"Chungcharoen, A\"\n486 \"Luwel, M\"\n487 \"Cozzens, S\"\n488 \"Pfeil, KM\"\n489 \"Bailon-moreno, R\"\n490 \"Kinnucan, MT\"\n491 \"Ortiz-rivera, LA\"\n492 \"Smeyers, M\"\n493 \"Schubert, A\"\n494 \"Bruins, EEW\"\n495 \"Cambrosio, A\"\n496 \"Breton-lopez, J\"\n497 \"Xu, B\"\n498 \"Schwarz, AW\"\n499 \"Ruts, C\"\n500 \"Turner, WA\"\n501 \"Bhattacharya, S\"\n502 \"Ramani, SV\"\n503 \"Rosenbaum, H\"\n504 \"Mehrotra, NN\"\n505 \"Romero, F\"\n506 \"De Vries, R\"\n507 \"Krauskopf, M\"\n508 \"Georghiou, L\"\n509 \"Kopcsa, A\"\n510 \"Houben, JA\"\n511 \"Hooten, PA\"\n512 \"Arapov, MV\"\n513 \"Mely, B\"\n514 \"Hartley, J\"\n515 \"Jouve, O\"\n516 \"Saitoh, Y\"\n517 \"Mehrdad, M\"\n518 \"Egghe, L\"\n519 \"Kint, A\"\n520 \"Harter, SP\"\n521 \"Ruiz-palomo, F\"\n522 \"Tang, R\"\n523 \"Sudhakar, P\"\n524 \"Albert, A\"\n525 \"Find, S\"\n526 \"Sebastian, J\"\n527 \"Luna-morales, ME\"\n528 \"Glanzel, W\"\n529 \"Spurling, TH\"\n530 \"Arreto, CD\"\n531 \"Hullmann, A\"\n532 \"Yu, G\"\n533 \"Criado, E\"\n534 \"Tseng, TM\"\n535 \"Taxt, RE\"\n536 \"Padhi, P\"\n537 \"Laudel, G\"\n538 \"Nagpaul, PS\"\n539 \"Van Hecke, P\"\n540 \"Schwechheimer, H\"\n541 \"Stephens, D\"\n542 \"Bassecoulard-zitt, E\"\n543 \"Anegon, FD\"\n544 \"Singh, SP\"\n545 \"Danowski, JA\"\n546 \"Nederhof, AJ\"\n547 \"Shirabe, M\"\n548 \"Faba-perez, C\"\n549 \"Vera, MI\"\n550 \"Ferreiro, L\"\n551 \"Demeis, L\"\n552 \"Sen, BK\"\n553 \"Magri, MH\"\n554 \"Kumar, S\"\n555 \"Mallik, M\"\n556 \"Laredo, P\"\n557 \"Guo, H\"\n558 \"Dillon, SM\"\n559 \"Porter, AL\"\n560 \"Christensen, FH\"\n561 \"Cami, J\"\n562 \"Ortega, C\"\n563 \"Ingwersen, P\"\n564 \"Jin, XY\"\n565 \"Bali, A\"\n566 \"Peck, C\"\n567 \"Rinia, EJ\"\n568 \"Hinze, S\"\n569 \"Granadino, B\"\n570 \"Granes, J\"\n571 \"Solorio-lagunas, J\"\n572 \"Cortes, HD\"\n573 \"Crouch, D\"\n574 \"Lui, JC\"\n575 \"Seetharam, G\"\n576 \"Liu, JW\"\n577 \"Garvey, WD\"\n578 \"Raina, D\"\n579 \"Takahashi, K\"\n580 \"Morris, SA\"\n581 \"Mendez, A\"\n582 \"Bedford, CD\"\n583 \"Fischer, AL\"\n584 \"Leger, MD\"\n585 \"Fox, C\"\n586 \"Mendez, I\"\n587 \"Tshiteya, R\"\n588 \"Michelet, B\"\n589 \"Mijangos-nolasco, A\"\n590 \"Sancho, R\"\n591 \"Jiang, GH\"\n592 \"Davenport, E\"\n593 \"Mustar, P\"\n594 \"Dutheuil, C\"\n595 \"Plaza, LM\"\n596 \"Banos, RR\"\n597 \"Py, Y\"\n598 \"Hoshuyama, T\"\n599 \"Ramirez, AM\"\n600 \"Phornsadja, K\"\n601 \"Wagner, CS\"\n602 \"Maisonneuve, H\"\n603 \"Suma, MP\"\n604 \"Conde, J\"\n605 \"Heinz, M\"\n606 \"Roy, A\"\n607 \"Xu, XS\"\n608 \"Roy, S\"\n609 \"Sharma, P\"\n610 \"He, TW\"\n611 \"Bentamar, D\"\n612 \"Vilanova, MR\"\n613 \"Von Tunzelmann, N\"\n614 \"Spruyt, E\"\n615 \"Escuder, MML\"\n616 \"Ibanez, JJ\"\n617 \"Olivastro, D\"\n618 \"Sharma, L\"\n619 \"Sangam, SL\"\n620 \"Oard, DW\"\n621 \"Chiu, CH\"\n622 \"Coates, V\"\n623 \"Guo, HN\"\n624 \"Zhu, L\"\n625 \"Nicolaisen, J\"\n626 \"Farooque, M\"\n627 \"Vanleeuwen, TN\"\n628 \"Miquel, JF\"\n629 \"Corrochano, MD\"\n630 \"Utecht, JT\"\n631 \"Hysen, K\"\n632 \"Shailendra, K\"\n*edges\n1 402 1\n1 615 3\n1 583 1\n3 75 1\n3 467 1\n3 509 1\n3 291 1\n4 307 1\n4 118 1\n4 120 4\n4 379 1\n4 15 1\n4 196 1\n4 219 3\n5 581 1\n6 421 1\n6 93 1\n7 352 1\n7 263 1\n8 477 1\n8 237 1\n9 283 1\n10 30 1\n10 395 1\n10 483 1\n11 551 2\n11 308 1\n11 264 1\n12 111 1\n12 485 1\n12 600 1\n12 73 1\n13 538 1\n14 580 1\n14 29 1\n14 42 1\n14 82 1\n15 120 1\n15 307 1\n15 196 1\n15 219 1\n16 392 7\n16 141 1\n17 187 1\n17 177 1\n17 437 1\n18 207 2\n18 188 2\n19 166 1\n20 80 1\n20 542 1\n20 115 1\n20 484 1\n20 293 1\n20 228 1\n20 218 3\n21 149 1\n21 225 1\n22 247 1\n22 441 1\n22 195 1\n22 353 1\n23 359 1\n23 40 2\n23 281 1\n23 382 1\n23 594 2\n23 226 1\n23 262 1\n23 429 1\n23 228 5\n23 168 7\n23 628 8\n23 602 1\n23 143 1\n24 410 1\n24 50 1\n25 120 1\n26 381 2\n26 176 1\n26 115 1\n27 420 1\n28 158 1\n29 580 1\n29 42 1\n29 82 1\n30 395 1\n30 483 1\n31 173 1\n31 253 1\n31 164 1\n32 230 1\n33 481 1\n33 35 1\n33 153 1\n34 385 1\n35 481 1\n35 153 1\n36 553 1\n36 410 1\n36 312 1\n36 482 1\n37 161 1\n39 501 1\n39 337 1\n40 281 1\n40 594 1\n40 262 1\n40 382 1\n40 628 2\n41 107 3\n41 97 1\n41 189 1\n41 114 1\n41 416 1\n41 172 4\n42 580 1\n42 82 1\n43 369 1\n43 124 1\n44 183 1\n44 184 1\n44 243 1\n44 588 1\n44 215 1\n44 131 1\n45 496 3\n45 124 3\n46 559 1\n47 605 1\n47 300 1\n48 455 2\n48 528 4\n49 449 1\n49 199 1\n49 603 1\n49 518 2\n49 575 1\n50 410 2\n51 541 1\n51 420 1\n51 343 1\n53 210 2\n53 108 2\n54 289 1\n54 68 1\n55 327 1\n56 396 1\n56 230 2\n56 314 3\n57 293 1\n57 183 4\n57 424 1\n57 304 1\n58 571 1\n58 193 1\n58 233 1\n58 403 1\n58 589 1\n59 578 1\n59 301 1\n60 428 1\n60 354 1\n60 297 1\n60 171 1\n60 124 1\n61 295 1\n62 252 1\n62 308 1\n62 282 1\n63 275 2\n63 587 1\n63 386 1\n64 393 1\n65 327 1\n65 303 1\n66 149 1\n67 449 1\n67 191 1\n67 499 1\n68 289 1\n68 109 1\n68 566 1\n68 74 1\n69 149 1\n69 121 1\n70 77 1\n71 275 1\n72 422 1\n72 427 1\n73 111 1\n73 485 1\n73 600 1\n74 566 1\n74 109 1\n75 467 1\n75 509 1\n75 291 1\n76 631 1\n76 114 1\n76 364 1\n76 423 1\n76 299 1\n77 558 1\n78 327 1\n79 449 1\n81 529 1\n81 287 3\n81 140 1\n82 580 1\n83 547 1\n83 401 1\n84 175 1\n84 301 1\n85 105 1\n86 246 1\n87 388 2\n87 275 4\n87 458 1\n87 135 1\n87 210 1\n87 347 1\n87 587 1\n87 488 1\n87 599 1\n88 595 3\n88 616 1\n88 99 1\n88 586 1\n89 377 6\n89 389 1\n89 581 1\n89 106 1\n89 505 1\n89 213 2\n90 268 1\n90 559 1\n90 334 1\n90 442 1\n90 564 1\n91 194 1\n92 340 1\n92 559 1\n92 146 1\n92 626 1\n92 622 1\n92 160 1\n93 421 1\n94 188 1\n94 269 1\n94 360 1\n95 543 1\n95 596 2\n95 183 2\n95 472 2\n95 267 3\n95 489 2\n96 444 1\n96 162 1\n97 432 1\n97 107 1\n97 416 2\n97 172 1\n98 628 1\n99 595 1\n99 165 5\n99 148 1\n100 149 5\n100 212 2\n100 475 1\n101 451 1\n101 590 1\n103 527 2\n103 319 1\n104 149 1\n104 212 1\n105 305 1\n106 377 1\n107 416 1\n107 189 1\n107 522 1\n107 172 3\n108 210 2\n109 566 1\n110 517 1\n110 446 1\n110 431 1\n111 600 1\n111 485 1\n111 173 1\n112 398 1\n112 528 1\n112 362 1\n113 627 1\n113 214 1\n114 364 1\n114 299 1\n115 392 2\n115 542 1\n115 381 1\n115 176 2\n115 141 1\n117 512 1\n117 322 1\n118 120 1\n118 379 1\n118 425 1\n118 219 1\n119 579 1\n119 341 1\n119 598 1\n120 307 2\n120 379 1\n120 613 1\n120 528 1\n120 346 1\n120 443 1\n120 196 1\n120 219 3\n121 149 1\n121 411 1\n122 532 1\n123 553 1\n124 369 2\n124 297 1\n124 354 1\n124 428 1\n124 496 4\n124 171 1\n125 627 2\n126 490 1\n126 376 2\n127 507 1\n127 418 1\n127 137 1\n129 563 1\n129 415 1\n129 280 2\n130 520 1\n131 183 1\n131 243 1\n132 551 1\n132 282 2\n133 336 3\n133 546 1\n133 394 1\n133 627 10\n133 494 2\n133 214 3\n133 466 1\n133 298 4\n134 470 1\n135 388 4\n135 275 4\n135 383 1\n136 604 1\n136 356 1\n136 561 1\n136 313 1\n136 151 1\n136 152 1\n136 521 1\n136 433 1\n137 507 1\n137 418 1\n138 352 1\n138 263 1\n139 210 3\n139 248 1\n139 528 2\n140 529 1\n140 537 1\n141 392 4\n141 147 1\n141 230 1\n143 513 1\n143 168 1\n143 429 1\n143 228 2\n143 628 1\n144 550 1\n145 439 1\n146 340 1\n146 559 1\n146 626 1\n146 622 1\n146 160 1\n147 230 2\n147 314 1\n148 165 1\n149 225 1\n149 212 6\n149 528 2\n149 473 2\n149 355 1\n149 475 1\n150 498 1\n151 604 1\n151 356 1\n151 561 1\n151 313 1\n151 152 1\n151 521 1\n151 433 1\n152 604 1\n152 356 1\n152 561 1\n152 313 1\n152 521 1\n152 433 1\n153 481 1\n155 459 1\n155 201 2\n155 327 2\n156 178 1\n156 463 1\n156 309 1\n157 182 1\n159 272 1\n160 340 1\n160 559 1\n160 626 1\n160 622 1\n161 277 4\n161 506 1\n162 444 1\n163 224 4\n163 244 3\n164 173 1\n164 253 1\n166 592 1\n166 173 1\n167 598 1\n167 579 1\n167 273 1\n167 245 1\n167 181 1\n168 359 1\n168 435 1\n168 226 1\n168 429 1\n168 228 4\n168 628 4\n168 602 2\n169 224 1\n169 244 1\n170 598 1\n170 579 1\n170 273 1\n170 245 1\n170 181 1\n171 428 1\n171 354 1\n171 297 1\n172 189 1\n172 416 1\n173 325 2\n173 592 2\n173 423 3\n173 242 1\n173 503 1\n173 253 1\n173 366 1\n175 609 4\n175 301 1\n175 284 2\n175 371 2\n175 536 6\n175 554 1\n175 565 1\n175 358 5\n175 618 1\n176 381 1\n176 577 1\n177 491 1\n177 437 1\n177 236 1\n177 581 1\n177 223 1\n177 187 1\n178 621 1\n178 463 1\n178 309 3\n178 534 1\n179 563 1\n179 203 1\n179 241 1\n180 205 4\n181 273 2\n181 598 2\n181 579 2\n181 245 2\n182 480 1\n182 617 2\n182 257 1\n183 424 1\n183 489 2\n183 351 1\n183 596 2\n183 342 1\n183 293 2\n183 243 1\n183 588 1\n183 495 1\n183 457 1\n183 202 1\n183 304 1\n183 419 1\n183 597 1\n184 215 1\n184 588 1\n185 441 1\n186 617 1\n187 437 1\n188 460 1\n188 360 1\n188 327 1\n188 207 2\n188 269 1\n190 563 1\n190 198 1\n190 261 1\n191 449 1\n191 499 1\n192 217 2\n192 233 1\n192 285 2\n192 319 1\n193 571 1\n195 247 1\n195 441 1\n195 353 1\n196 307 1\n196 219 1\n197 314 1\n198 563 3\n198 261 1\n200 318 2\n201 459 1\n201 327 2\n203 563 1\n203 241 1\n204 210 1\n204 567 1\n204 528 1\n206 222 1\n206 279 1\n206 530 1\n208 449 1\n209 430 1\n210 388 1\n210 275 1\n210 581 1\n210 352 2\n210 528 16\n210 248 1\n210 567 1\n211 327 1\n212 528 2\n212 475 1\n213 377 8\n213 561 2\n213 505 1\n213 581 1\n215 588 1\n217 270 1\n217 285 2\n217 319 2\n217 445 1\n217 233 1\n218 293 1\n219 307 1\n219 379 1\n220 554 1\n220 301 1\n222 279 1\n222 530 1\n223 491 1\n224 244 4\n226 429 1\n226 228 1\n226 628 1\n227 399 1\n228 359 1\n228 513 1\n228 429 2\n228 628 6\n229 561 1\n230 576 1\n230 624 1\n230 449 1\n230 434 1\n230 314 3\n230 476 1\n230 321 1\n231 305 1\n232 319 1\n233 285 1\n233 403 1\n233 319 2\n234 355 1\n235 327 1\n237 477 1\n238 563 1\n239 548 1\n241 563 1\n242 325 1\n242 503 1\n245 598 2\n245 579 2\n245 273 2\n247 353 1\n247 441 1\n249 479 1\n249 288 1\n251 326 1\n251 256 1\n251 440 1\n252 282 1\n254 584 1\n254 500 1\n254 414 1\n256 326 1\n256 440 1\n257 617 1\n258 570 1\n258 445 1\n258 324 1\n259 559 1\n261 563 1\n262 281 1\n262 594 1\n262 382 1\n262 628 1\n263 568 1\n263 352 4\n263 471 1\n264 551 1\n267 543 1\n267 472 2\n268 564 1\n268 334 1\n268 442 1\n268 559 1\n269 360 1\n270 319 1\n271 590 1\n271 533 1\n273 598 2\n273 579 2\n274 525 1\n274 498 2\n275 388 5\n275 397 1\n275 347 2\n275 488 1\n275 620 1\n275 582 1\n275 572 1\n275 317 1\n275 599 1\n275 587 2\n275 383 1\n275 458 1\n275 380 1\n275 386 1\n276 619 1\n276 618 1\n276 554 3\n276 609 1\n276 301 9\n277 612 1\n277 361 1\n277 409 1\n277 487 1\n277 601 1\n279 530 1\n280 563 1\n280 415 1\n281 594 1\n281 382 1\n281 628 1\n282 448 1\n282 551 1\n282 316 1\n282 375 1\n283 308 1\n284 565 1\n285 319 1\n286 551 1\n286 323 2\n286 308 2\n288 479 1\n290 559 1\n290 574 1\n291 467 1\n291 509 1\n292 630 1\n293 342 1\n293 495 1\n294 504 1\n294 301 1\n295 404 1\n295 474 1\n296 407 2\n296 327 1\n297 428 1\n297 354 1\n298 494 2\n298 627 4\n299 364 1\n300 398 1\n300 528 4\n301 619 1\n301 609 2\n301 618 1\n301 417 1\n301 449 1\n301 578 2\n301 314 1\n301 554 6\n301 504 1\n302 306 1\n302 516 1\n303 327 1\n305 413 2\n306 368 1\n306 547 2\n306 516 1\n308 327 1\n308 551 2\n308 323 2\n309 621 1\n309 463 1\n309 534 1\n311 427 2\n312 515 1\n312 410 1\n312 482 1\n312 439 1\n312 606 1\n312 553 1\n312 348 1\n313 604 1\n313 356 1\n313 561 1\n313 521 1\n313 433 1\n314 449 1\n314 501 1\n314 528 1\n315 585 1\n315 514 1\n316 448 1\n316 375 1\n318 563 2\n318 560 1\n319 527 1\n320 447 1\n320 500 1\n321 591 1\n321 557 1\n321 339 1\n321 372 1\n321 454 1\n321 528 1\n321 434 1\n321 449 1\n321 497 1\n321 363 1\n321 476 1\n322 512 1\n323 551 1\n324 570 1\n324 445 1\n325 592 1\n325 503 1\n326 440 1\n327 460 1\n327 407 1\n327 436 1\n327 331 1\n327 459 1\n328 532 1\n329 438 1\n329 400 1\n330 563 2\n334 564 1\n334 442 1\n334 559 1\n335 377 4\n336 546 1\n336 627 4\n336 466 1\n337 501 1\n339 557 1\n339 454 1\n339 483 1\n339 372 1\n339 497 1\n339 363 1\n340 559 1\n340 626 1\n340 622 1\n341 579 1\n341 598 1\n342 495 1\n343 541 1\n343 420 1\n344 591 2\n344 408 2\n344 607 1\n345 478 1\n345 411 2\n347 397 1\n347 582 1\n347 572 1\n347 599 2\n347 458 2\n348 515 1\n348 606 1\n350 392 1\n351 419 1\n351 597 1\n351 457 1\n352 568 3\n352 471 1\n352 528 2\n353 441 1\n354 428 1\n356 604 1\n356 561 1\n356 521 1\n356 433 1\n357 449 1\n358 609 1\n359 628 1\n362 398 1\n362 528 1\n363 557 1\n363 454 1\n363 372 1\n363 497 1\n367 399 1\n370 392 3\n371 554 1\n371 609 1\n372 557 1\n372 454 1\n372 497 1\n373 387 3\n374 595 1\n374 562 1\n375 448 1\n377 389 1\n377 581 4\n377 505 1\n377 561 1\n378 500 1\n380 620 1\n382 594 1\n382 628 1\n383 388 1\n384 493 1\n390 482 1\n394 627 1\n395 483 1\n397 582 1\n397 572 1\n398 528 7\n400 545 1\n400 438 1\n402 615 1\n404 474 1\n406 519 1\n406 510 2\n406 614 1\n407 555 1\n408 591 2\n408 607 1\n410 482 1\n410 553 1\n411 535 1\n411 478 1\n412 590 1\n413 507 1\n413 464 1\n413 549 1\n414 584 1\n414 500 1\n416 432 1\n418 507 1\n419 597 1\n419 457 1\n420 541 1\n420 468 1\n422 427 1\n426 449 1\n426 539 1\n427 528 1\n427 540 2\n429 513 1\n429 628 1\n431 517 1\n431 446 1\n433 604 1\n433 561 1\n433 521 1\n434 476 1\n435 602 1\n442 564 1\n442 559 1\n443 613 1\n445 570 1\n446 517 1\n447 500 1\n449 591 1\n449 554 1\n449 499 1\n449 450 3\n449 492 1\n449 528 1\n449 576 1\n449 539 1\n449 518 11\n449 468 1\n451 590 1\n452 520 1\n453 507 1\n454 557 1\n454 497 1\n455 528 2\n456 483 1\n457 597 1\n458 599 2\n461 528 3\n462 507 1\n462 549 1\n464 507 1\n464 549 1\n465 552 1\n466 627 1\n467 509 1\n468 611 1\n468 518 1\n469 481 1\n482 553 1\n483 623 1\n485 600 1\n488 587 1\n489 596 2\n494 627 2\n497 557 1\n498 525 1\n500 584 1\n501 544 1\n501 523 1\n507 549 3\n510 519 1\n510 614 1\n511 520 1\n514 585 1\n515 606 1\n521 604 1\n521 561 1\n523 544 1\n524 595 1\n528 591 1\n528 567 1\n533 590 1\n534 621 1\n538 608 1\n538 618 2\n543 629 1\n546 627 1\n552 632 1\n554 619 1\n554 609 2\n556 593 1\n559 574 1\n559 626 1\n559 622 1\n559 564 1\n560 563 3\n561 604 1\n562 595 1\n572 582 1\n579 598 3\n583 615 1\n586 616 1\n591 607 1\n594 628 2\n609 618 1\n622 626 1\n"
  },
  {
    "path": "examples/inf2005-2009.net",
    "content": "*network Informetrics2005-2009\n*vertices 634\n1 \"Pereira, JCR\"\n2 \"Harthorn, BH\"\n3 \"Verbeek, A\"\n4 \"Fischer, TC\"\n5 \"Van Den Besselaar, P\"\n6 \"Ottoson, JM\"\n7 \"Bollen, J\"\n8 \"Engels, A\"\n9 \"Calo, WA\"\n10 \"Diaz-perez, M\"\n11 \"Barth, RB\"\n12 \"Bhattacharya, S\"\n13 \"Huang, Y\"\n14 \"Zhou, P\"\n15 \"Van Looy, B\"\n16 \"Garcia-zorita, C\"\n17 \"You, J\"\n18 \"Zitt, M\"\n19 \"Buxton, M\"\n20 \"Wang, XH\"\n21 \"Savanur, K\"\n22 \"Van Raan, AFJ\"\n23 \"Harirchi, G\"\n24 \"Liu, NC\"\n25 \"Leggat, PA\"\n26 \"Klitkou, A\"\n27 \"Tukeva, T\"\n28 \"Ausloos, M\"\n29 \"Rushenberg, RL\"\n30 \"Sorenson, MM\"\n31 \"Arora, P\"\n32 \"Schneider, JW\"\n33 \"Hurt, I\"\n34 \"De Zeeuw, D\"\n35 \"Wilkinson, D\"\n36 \"Ruschenburg, T\"\n37 \"Robinson, S\"\n38 \"Kretschmer, H\"\n39 \"Torres-salinas, D\"\n40 \"Havemann, F\"\n41 \"Thijs, B\"\n42 \"Rao, IKR\"\n43 \"Ho, YS\"\n44 \"Yang, K\"\n45 \"Mccain, KW\"\n46 \"Johnson, SD\"\n47 \"Izquierdo, LR\"\n48 \"Kundra, R\"\n49 \"Maciaschapula, CA\"\n50 \"Wang, MH\"\n51 \"Barden, EM\"\n52 \"Corera-alvarez, E\"\n53 \"Chaimovich, H\"\n54 \"Buela-casal, G\"\n55 \"Conti, J\"\n56 \"Karageorgopoulos, DE\"\n57 \"Beirlant, J\"\n58 \"Salmeron, JL\"\n59 \"Chaudet, H\"\n60 \"Gehanno, JF\"\n61 \"Oppenheim, C\"\n62 \"Barriga, OA\"\n63 \"Weingart, P\"\n64 \"Wilcox-jay, K\"\n65 \"Rogers, Y\"\n66 \"Ahmed, T\"\n67 \"Vaughan, L\"\n68 \"Pearson, DC\"\n69 \"Trillo-dominguez, M\"\n70 \"Weber, J\"\n71 \"Hardeman, S\"\n72 \"Shapira, P\"\n73 \"Payne, N\"\n74 \"Krauskopf, E\"\n75 \"Shi, F\"\n76 \"Fujigaki, Y\"\n77 \"Cincera, M\"\n78 \"Krauskopf, M\"\n79 \"Navarro, A\"\n80 \"Humenik, JA\"\n81 \"Garcia-santiago, L\"\n82 \"Santa, S\"\n83 \"Ramanana-rahary, S\"\n84 \"Chu, DC\"\n85 \"Contreras, EJ\"\n86 \"Galvez, C\"\n87 \"Fowler, JH\"\n88 \"Martin-sempere, MJ\"\n89 \"Melin, G\"\n90 \"Geisler, E\"\n91 \"Karpouzian, G\"\n92 \"Antell, K\"\n93 \"Johnson, B\"\n94 \"Iversen, EJ\"\n95 \"Summers, MAC\"\n96 \"Yu, G\"\n97 \"Li, XM\"\n98 \"Markusova, VA\"\n99 \"Marques-portella, C\"\n100 \"Smith, DMS\"\n101 \"Girji, RM\"\n102 \"Redpath, S\"\n103 \"Debackere, K\"\n104 \"Hazelton, M\"\n105 \"Yu, DR\"\n106 \"Shlesinger, MF\"\n107 \"Janssens, F\"\n108 \"Ivanov, VV\"\n109 \"Levitt, JM\"\n110 \"Lynd, FE\"\n111 \"Hamre, R\"\n112 \"Rousseau, S\"\n113 \"Tolles, WM\"\n114 \"Alexandrov, M\"\n115 \"Dastidar, PG\"\n116 \"Groneberg-kloft, B\"\n117 \"Heydari, A\"\n118 \"Heinze, T\"\n119 \"Saenz-feijoo, R\"\n120 \"Niemi, T\"\n121 \"Chen, SR\"\n122 \"Boerner, K\"\n123 \"Henadeera, K\"\n124 \"Lai, KK\"\n125 \"Cornelius, B\"\n126 \"Zuccala, A\"\n127 \"Leviton, L\"\n128 \"Zhang, J2\"\n129 \"Rollin, L\"\n130 \"Campbell, D\"\n131 \"Davis, M\"\n132 \"Meyer, M\"\n133 \"Bermudez-sanchez, MP\"\n134 \"Cole, FTH\"\n135 \"Origgi, G\"\n136 \"Jeong, S\"\n137 \"Levene, M\"\n138 \"Dodbele, S\"\n139 \"Arencibia-jorge, R\"\n140 \"De Filippo, D\"\n141 \"Dorta-contreras, AJ\"\n142 \"Icenhour, AS\"\n143 \"Persson, O\"\n144 \"Ke, W\"\n145 \"Okubo, Y\"\n146 \"Grobbee, DE\"\n147 \"Holden, G\"\n148 \"Hou, HY\"\n149 \"Liu, YX\"\n150 \"Perianes-rodriguez, A\"\n151 \"Kluver, R\"\n152 \"Pecht, M\"\n153 \"Li, LL\"\n154 \"Medina, A\"\n155 \"Lambiotte, R\"\n156 \"Kuusi, O\"\n157 \"De Sompel, HV\"\n158 \"Ovalle-perandones, MA\"\n159 \"Wouters, P\"\n160 \"White, HD\"\n161 \"Lee, W\"\n162 \"Perakakis, P\"\n163 \"Mauleon, E\"\n164 \"Morillo, F\"\n165 \"Rojas-sola, JI\"\n166 \"Soteriades, ES\"\n167 \"Harries, G\"\n168 \"Laine, T\"\n169 \"Briggs, MB\"\n170 \"Somogyi, A\"\n171 \"Archambault, E\"\n172 \"Olmeda-gomez, C\"\n173 \"Garg, KC\"\n174 \"Sanz, E\"\n175 \"Chiu, WT\"\n176 \"Yalpani, M\"\n177 \"Youtie, J\"\n178 \"Frenken, K\"\n179 \"Larsen, B\"\n180 \"Hussinger, K\"\n181 \"Bonitz, M\"\n182 \"Mcmillan, GS\"\n183 \"Meng, W\"\n184 \"Granadino, B\"\n185 \"Gutierrez-martinez, O\"\n186 \"Glenisson, P\"\n187 \"Chaichio-moreno, JA\"\n188 \"Grant, J\"\n189 \"Fairclough, R\"\n190 \"Munoz-munoz, AM\"\n191 \"Stuart, D\"\n192 \"Block, JA\"\n193 \"Bravo-vinaja, A\"\n194 \"Yuan, XJ\"\n195 \"Pereyra, AS\"\n196 \"Csajbok, E\"\n197 \"Tuunanen, T\"\n198 \"Libkind, I\"\n199 \"Mendez-vasquez, RI\"\n200 \"Hullmann, A\"\n201 \"Munoz-fernandez, FJ\"\n202 \"De Osma, ER\"\n203 \"Sahoo, BB\"\n204 \"Gleiser, S\"\n205 \"Bjorneborn, L\"\n206 \"Famoye, F\"\n207 \"Zhang, WW\"\n208 \"Ramirez, AMU\"\n209 \"De Moor, B\"\n210 \"Kurtenbach, E\"\n211 \"Czarnitzki, D\"\n212 \"Yang, LY\"\n213 \"Sen, BK\"\n214 \"Oncu, S\"\n215 \"Yi, H\"\n216 \"Proot, G\"\n217 \"Hoekman, J\"\n218 \"Braun, T\"\n219 \"Paraje, G\"\n220 \"Han, W\"\n221 \"Danell, R\"\n222 \"Ye, FY\"\n223 \"Noyons, ECM\"\n224 \"Welte, T\"\n225 \"Ortega, JL\"\n226 \"Jian, WS\"\n227 \"Fernandez-lopez, JA\"\n228 \"Robert, C\"\n229 \"Biswas, BC\"\n230 \"Pellegrino, D\"\n231 \"Kim, HG\"\n232 \"Scharnhorst, A\"\n233 \"Ajiferuke, I\"\n234 \"Cahill, CL\"\n235 \"Vasconcelos, SMR\"\n236 \"Liang, LM\"\n237 \"Jamal, T\"\n238 \"Thelwall, M\"\n239 \"Barker, K\"\n240 \"Aguillo, IF\"\n241 \"Gomez-crisostomo, R\"\n242 \"Zimmerman, E\"\n243 \"Maura-sardo, M\"\n244 \"Berhidi, A\"\n245 \"Marquiss, M\"\n246 \"Bliziotis, IA\"\n247 \"Cami, J\"\n248 \"Martin, J\"\n249 \"Martin, H\"\n250 \"Torricella-morales, RG\"\n251 \"Chinchilla-rodriguez, Z\"\n252 \"Zsindely, S\"\n253 \"Wu, SJ\"\n254 \"Yamashita, Y\"\n255 \"Valdes, CC\"\n256 \"Dhiensa, R\"\n257 \"Covert-vail, L\"\n258 \"Michan, L\"\n259 \"Schoon, ML\"\n260 \"Morse, SA\"\n261 \"Penumarthy, S\"\n262 \"Pau, MRD\"\n263 \"Navarrete-cortes, J\"\n264 \"Gingras, Y\"\n265 \"Devos, P\"\n266 \"Schmoch, U\"\n267 \"Peck, C\"\n268 \"Nelson, MJ\"\n269 \"Delgado-lopez-cozar, E\"\n270 \"Ding, GH\"\n271 \"Roessner, D\"\n272 \"Cohen, SA\"\n273 \"Rajendram, R\"\n274 \"Hassan-montero, Y\"\n275 \"Salvucci, LJ\"\n276 \"Leydesdorff, L\"\n277 \"Jarvelin, K\"\n278 \"Dutt, B\"\n279 \"Holloway, T\"\n280 \"Sunen-pinyol, E\"\n281 \"Figueira, I\"\n282 \"Iribarren-maestro, I\"\n283 \"Mendonca-araujo, K\"\n284 \"Scutaru, C\"\n285 \"Shin, J\"\n286 \"Hamilton, RD\"\n287 \"Pulgar, R\"\n288 \"Paravic-klijn, T\"\n289 \"Manjunath, M\"\n290 \"Lecocq, C\"\n291 \"Vanhooydonk, G\"\n292 \"Le Jean, T\"\n293 \"Suarez, E\"\n294 \"Xi, Z\"\n295 \"Mendoza-parra, S\"\n296 \"Sadana, R\"\n297 \"Quevedo-blasco, R\"\n298 \"Smith, AG\"\n299 \"Wooding, S\"\n300 \"Gupta, BM\"\n301 \"Cole, V\"\n302 \"Chuang, KY\"\n303 \"Must, U\"\n304 \"Garfield, E\"\n305 \"Tomizawa, H\"\n306 \"Evanco, W\"\n307 \"Veugelers, R\"\n308 \"Smith, C\"\n309 \"Nappila, T\"\n310 \"Smith, A\"\n311 \"Frame, I\"\n312 \"Kretschmer, U\"\n313 \"Kretschmer, T\"\n314 \"Leta, J\"\n315 \"Qian, WH\"\n316 \"Jurado-alameda, E\"\n317 \"Miguel, S\"\n318 \"Li, T\"\n319 \"Verner, JM\"\n320 \"Ostrowski, AD\"\n321 \"Li, Z\"\n322 \"Barbati, AD\"\n323 \"Del Jesus, MIV\"\n324 \"Sun, XX\"\n325 \"Oldenburg, BF\"\n326 \"Casey, DL\"\n327 \"Rodriguez, MA\"\n328 \"De La Moneda, M\"\n329 \"De La Potterie, BV\"\n330 \"Russell, JM\"\n331 \"Shachaf, P\"\n332 \"Cervello, R\"\n333 \"Kuhlmann, S\"\n334 \"Cantu, AG\"\n335 \"Garcia-martinez, AT\"\n336 \"Gamber, T\"\n337 \"Lewison, G\"\n338 \"Perreault, M\"\n339 \"Ramachandran, S\"\n340 \"Park, HW\"\n341 \"Rowlands, I\"\n342 \"Visser, MS\"\n343 \"Cronin, B\"\n344 \"Chung, KF\"\n345 \"Li, CY\"\n346 \"Lucio-arias, D\"\n347 \"Andrews, J\"\n348 \"Shan, S\"\n349 \"Gaudy, JF\"\n350 \"Coleman, A\"\n351 \"Murday, JS\"\n352 \"Buchtel, HA\"\n353 \"Karavasiou, AI\"\n354 \"Rosenberg, G\"\n355 \"Norris, M\"\n356 \"Munoz-molina, A\"\n357 \"Feng, N\"\n358 \"Kipp, MEI\"\n359 \"Grupp, H\"\n360 \"Bowles, CA\"\n361 \"Vasconcellos, JP\"\n362 \"Torres, G\"\n363 \"Kyvik, S\"\n364 \"Benson, D\"\n365 \"Friedrich-nishio, M\"\n366 \"Liu, ZY\"\n367 \"Upham, P\"\n368 \"Diospatonyi, I\"\n369 \"Harzing, AW\"\n370 \"Franke, K\"\n371 \"Van Der Wal, R\"\n372 \"Dhawan, SM\"\n373 \"Guns, R\"\n374 \"Kusma, B\"\n375 \"Michalopoulos, AS\"\n376 \"Smith, DR\"\n377 \"Cothey, V\"\n378 \"Rodriguez, V\"\n379 \"Rios, C\"\n380 \"Senter, SK\"\n381 \"Gil-montoya, JA\"\n382 \"Osareh, F\"\n383 \"Lovegrove, BG\"\n384 \"Hislop, GW\"\n385 \"Hellsten, I\"\n386 \"Ma, Z\"\n387 \"Hong, HD\"\n388 \"Carbonez, A\"\n389 \"Callaert, J\"\n390 \"Bordons, M\"\n391 \"Salmela, R\"\n392 \"Rojo, R\"\n393 \"Vasudevan, R\"\n394 \"Ao, XL\"\n395 \"Borlund, P\"\n396 \"Malpohl, G\"\n397 \"Liu, L\"\n398 \"Devillard, J\"\n399 \"Donovan, C\"\n400 \"Stump, JA\"\n401 \"Wilson, CS\"\n402 \"Mustar, P\"\n403 \"Burrell, QL\"\n404 \"Van Der Wurff, LJ\"\n405 \"Yoon, B\"\n406 \"Zador, E\"\n407 \"Luna-morales, ME\"\n408 \"Leemans, H\"\n409 \"Mogoutov, A\"\n410 \"Checa, P\"\n411 \"Hayashi, T\"\n412 \"Beltran, CL\"\n413 \"Wu, YS\"\n414 \"Liberman, S\"\n415 \"Ambre, S\"\n416 \"Koley, S\"\n417 \"Daim, TU\"\n418 \"Aksnes, DW\"\n419 \"Moreno, L\"\n420 \"Jirina, M\"\n421 \"Kouranos, VD\"\n422 \"Korn, A\"\n423 \"Vann, K\"\n424 \"Lau, CGY\"\n425 \"Lisee, C\"\n426 \"Ball, R\"\n427 \"Horlesberger, M\"\n428 \"Butler, L\"\n429 \"Gouveia, FC\"\n430 \"Cahlik, T\"\n431 \"Zhou, F\"\n432 \"Rafols, I\"\n433 \"Lozano, S\"\n434 \"Bar-ilan, J\"\n435 \"Shaw, W\"\n436 \"Guerrero-bote, VP\"\n437 \"Mendlowicz, M\"\n438 \"Costas, R\"\n439 \"De Pedro-cuesta, J\"\n440 \"Chen, Y\"\n441 \"Martin-moreno, C\"\n442 \"Frandsen, TF\"\n443 \"Rey-rocha, J\"\n444 \"Heerspink, HJL\"\n445 \"Herrero-solana, V\"\n446 \"Gulbrandsen, M\"\n447 \"Lal, K\"\n448 \"Lelu, A\"\n449 \"Courtial, JP\"\n450 \"Mercier, S\"\n451 \"Rousseau, R\"\n452 \"Lopez-illescas, C\"\n453 \"Braam, RR\"\n454 \"Boell, SK\"\n455 \"Jeannin, P\"\n456 \"Teng, LR\"\n457 \"Xie, SD\"\n458 \"Tijssen, R\"\n459 \"Prieto, JA\"\n460 \"Van Zeebroeck, N\"\n461 \"Furusawa, L\"\n462 \"Senker, J\"\n463 \"Meho, LI\"\n464 \"Holmberg, K\"\n465 \"Paley, WB\"\n466 \"Gerdsri, P\"\n467 \"Pan, YT\"\n468 \"Schlemmer, B\"\n469 \"Coutinho, E\"\n470 \"Lima, M\"\n471 \"Clay, MA\"\n472 \"Louvel, A\"\n473 \"Klavans, R\"\n474 \"Lee, S\"\n475 \"Tunger, D\"\n476 \"Echermane, A\"\n477 \"Schoeneck, DJ\"\n478 \"Lascurain-sanchez, ML\"\n479 \"Jansen, D\"\n480 \"Schiebel, E\"\n481 \"Nygaard, S\"\n482 \"Angus, E\"\n483 \"Chen, L2\"\n484 \"Boyack, KW\"\n485 \"Meiss, M\"\n486 \"Ruiz-perez, R\"\n487 \"Crisostomo, RG\"\n488 \"Jonkers, K\"\n489 \"Araujo-ruiz, JA\"\n490 \"Inzelt, A\"\n491 \"Mcallister, RRJ\"\n492 \"Wang, Y\"\n493 \"Srivastava, D\"\n494 \"Chou, CT\"\n495 \"Olsen, TB\"\n496 \"Nikodym, KF\"\n497 \"Jin, BH\"\n498 \"Llamas, G\"\n499 \"Chiang, CH\"\n500 \"Kushmerick, A\"\n501 \"Onghena, P\"\n502 \"Bailon-moreno, R\"\n503 \"Donnadieu, S\"\n504 \"Schubert, T\"\n505 \"Ladner, J\"\n506 \"Heimeriks, G\"\n507 \"Hu, ZH\"\n508 \"Du Plessis, M\"\n509 \"Hanney, S\"\n510 \"Cambrosio, A\"\n511 \"Zamora, H\"\n512 \"Schubert, M\"\n513 \"Laudel, G\"\n514 \"Vargas-quesada, B\"\n515 \"Lin, A\"\n516 \"Cabizuca, M\"\n517 \"Porter, AL\"\n518 \"Mancini, J\"\n519 \"Beery, WL\"\n520 \"Knol, MJ\"\n521 \"Gao, YJ\"\n522 \"Ortiz, AP\"\n523 \"Hartley, J\"\n524 \"Utrilla, AM\"\n525 \"Mehrdad, M\"\n526 \"Egghe, L\"\n527 \"Zych, I\"\n528 \"Tang, R\"\n529 \"Tang, P\"\n530 \"Albert, A\"\n531 \"Karypis, G\"\n532 \"Glanzel, W\"\n533 \"Nicholas, D\"\n534 \"Arreto, CD\"\n535 \"Vasas, L\"\n536 \"Buter, RK\"\n537 \"Wolfram, D\"\n538 \"Yu, H\"\n539 \"Lee, JY\"\n540 \"Glaser, J\"\n541 \"Danell, JAB\"\n542 \"Hoorens, S\"\n543 \"Zapico-alonso, F\"\n544 \"Janssen, MA\"\n545 \"Taylor, M\"\n546 \"Anegon, FD\"\n547 \"Prabowo, R\"\n548 \"Panos, G\"\n549 \"Fernandez, MT\"\n550 \"Burgoon, J\"\n551 \"Marti-lahera, Y\"\n552 \"Faba-perez, C\"\n553 \"Fieschi, M\"\n554 \"Song, IY\"\n555 \"Lariviere, V\"\n556 \"Wald, A\"\n557 \"Landstrom, H\"\n558 \"Pfeil, KM\"\n559 \"Kousha, K\"\n560 \"Dinh, QT\"\n561 \"Small, H\"\n562 \"Wen, HC\"\n563 \"Nicol, MB\"\n564 \"Ingwersen, P\"\n565 \"Lang, PB\"\n566 \"Bauer, G\"\n567 \"Guo, R\"\n568 \"Zhang, L\"\n569 \"De Craen, AJM\"\n570 \"Bontems, V\"\n571 \"Kostoff, RN\"\n572 \"Cortes, HD\"\n573 \"Bahl, M\"\n574 \"Eowles, CA\"\n575 \"Wanless, S\"\n576 \"Takahashi, K\"\n577 \"Telcs, A\"\n578 \"Falagas, ME\"\n579 \"Wu, CZ\"\n580 \"Mendez, B\"\n581 \"Koytcheff, RG\"\n582 \"Madhavi, Y\"\n583 \"Young, T\"\n584 \"Del Rio, JA\"\n585 \"Larowe, G\"\n586 \"Varshavskii, AE\"\n587 \"Pepe, A\"\n588 \"Barjak, F\"\n589 \"Kahane, B\"\n590 \"Shaw, D\"\n591 \"Sancho, R\"\n592 \"Caillieux, N\"\n593 \"Preedy, VR\"\n594 \"Greenwald, HP\"\n595 \"Espinosa-calvo, ME\"\n596 \"Collazo-reyes, F\"\n597 \"Plaza, LM\"\n598 \"Banos, RR\"\n599 \"Vignola-gagne, E\"\n600 \"Paraschakis, K\"\n601 \"Vergidis, PI\"\n602 \"Nebelong-bonnevie, E\"\n603 \"Vadillo-munoz, O\"\n604 \"Da Luz, MP\"\n605 \"Wagner, CS\"\n606 \"Hardy, E\"\n607 \"Horowitz, M\"\n608 \"Heinz, M\"\n609 \"Braga, RJ\"\n610 \"Roy, A\"\n611 \"Cote, G\"\n612 \"Jansz, CNM\"\n613 \"Perez-angon, MA\"\n614 \"Roy, S\"\n615 \"He, TW\"\n616 \"Cassiman, B\"\n617 \"Kumar, S\"\n618 \"Tshiteya, R\"\n619 \"Quarcoo, D\"\n620 \"Kim, H\"\n621 \"Sangam, SL\"\n622 \"Etemad, S\"\n623 \"Magerman, T\"\n624 \"Guo, HC\"\n625 \"Keating, P\"\n626 \"Panzarasa, P\"\n627 \"Nicolaisen, J\"\n628 \"Cruset, AL\"\n629 \"Vanleeuwen, TN\"\n630 \"Hoffmann, U\"\n631 \"Utecht, JT\"\n632 \"Hsu, YHE\"\n633 \"Van Mackelenbergh, M\"\n634 \"Rivett, DA\"\n*edges\n1 53 1\n1 322 1\n1 314 1\n1 361 1\n1 461 1\n2 33 1\n2 55 1\n2 249 1\n2 320 1\n3 103 2\n3 41 1\n3 389 1\n3 15 1\n4 619 1\n4 116 1\n4 374 1\n4 284 1\n4 224 1\n5 506 1\n6 111 1\n6 594 1\n6 127 1\n6 380 1\n6 234 1\n6 519 1\n6 68 1\n7 327 2\n7 157 1\n8 63 1\n8 36 1\n9 243 1\n9 522 1\n9 293 1\n11 571 6\n11 360 3\n11 138 3\n11 169 2\n11 496 3\n11 12 3\n11 424 2\n11 142 4\n11 152 3\n11 29 3\n11 574 1\n12 571 4\n12 360 2\n12 138 3\n12 31 1\n12 496 3\n12 152 3\n12 142 3\n12 169 1\n12 29 2\n12 574 1\n13 394 1\n14 41 1\n14 276 3\n14 532 1\n15 389 2\n15 41 1\n15 623 1\n15 103 3\n15 458 1\n15 290 1\n15 186 1\n15 616 1\n16 478 2\n16 174 3\n16 441 2\n17 67 2\n18 451 1\n18 448 1\n18 83 2\n19 337 1\n19 299 1\n19 311 1\n19 509 2\n19 583 1\n19 188 2\n20 105 1\n20 96 1\n21 621 2\n21 289 2\n21 393 1\n22 390 1\n22 266 1\n22 629 1\n22 438 1\n23 622 1\n23 89 1\n24 397 1\n25 376 1\n26 94 1\n26 446 1\n26 481 1\n27 631 1\n27 508 1\n28 232 1\n28 238 1\n28 155 2\n28 334 1\n28 385 1\n29 571 3\n29 360 2\n29 138 2\n29 169 2\n29 152 3\n29 142 3\n29 496 2\n29 574 1\n30 314 1\n30 235 1\n32 179 1\n32 564 1\n32 395 2\n33 249 1\n33 55 1\n33 320 1\n34 444 1\n34 629 1\n34 146 1\n34 520 1\n35 97 2\n36 63 1\n37 97 1\n37 588 1\n38 148 1\n38 312 1\n38 313 3\n38 366 1\n38 630 1\n38 588 1\n39 502 1\n39 598 1\n39 269 2\n39 85 2\n40 608 1\n40 236 1\n41 389 1\n41 103 4\n41 314 2\n41 468 2\n41 532 13\n41 107 1\n42 451 1\n42 203 2\n42 526 2\n42 149 1\n43 579 1\n43 121 1\n43 153 1\n43 624 1\n43 321 1\n43 357 1\n43 315 1\n43 632 1\n43 302 1\n43 50 1\n43 318 1\n43 457 1\n43 270 1\n43 215 1\n43 207 1\n43 394 1\n43 431 1\n43 226 1\n43 175 3\n43 562 1\n43 345 1\n45 306 1\n45 382 1\n45 301 1\n45 384 1\n45 275 1\n45 319 1\n46 383 1\n47 544 1\n47 491 1\n47 100 1\n48 337 1\n50 357 1\n50 270 1\n50 153 1\n51 212 1\n52 356 2\n52 251 2\n52 445 2\n52 201 2\n52 514 2\n53 314 1\n54 297 1\n54 527 1\n54 162 1\n54 133 1\n54 185 1\n54 362 1\n54 433 1\n54 410 1\n54 187 1\n54 603 1\n54 323 1\n54 379 1\n54 154 1\n54 263 1\n54 545 1\n55 249 1\n55 320 1\n56 578 1\n56 421 1\n57 388 1\n57 408 1\n57 532 1\n58 433 1\n59 518 1\n59 553 1\n60 505 1\n60 70 1\n60 265 1\n60 576 2\n60 129 1\n60 435 1\n60 292 1\n60 376 1\n60 472 1\n61 390 1\n61 355 1\n61 126 2\n61 163 1\n61 256 2\n61 93 1\n61 66 1\n61 95 1\n61 267 1\n62 190 1\n62 85 1\n62 295 1\n62 288 1\n64 188 1\n64 337 1\n64 299 1\n66 267 1\n66 93 1\n67 521 2\n67 358 1\n67 590 2\n67 532 1\n68 111 1\n68 594 1\n68 127 1\n68 380 1\n68 234 1\n68 519 1\n70 576 1\n71 178 1\n71 217 1\n72 477 1\n72 517 2\n72 462 1\n72 118 1\n72 333 1\n72 177 3\n74 78 2\n74 580 1\n75 451 1\n75 236 1\n77 307 1\n77 329 1\n78 580 1\n79 110 1\n79 248 1\n80 571 1\n80 618 1\n80 531 1\n80 558 1\n82 381 1\n82 263 1\n82 287 1\n83 451 1\n84 494 1\n84 499 1\n85 202 1\n85 449 1\n85 288 1\n85 486 5\n85 598 3\n85 316 1\n85 445 1\n85 269 4\n85 190 1\n85 295 1\n85 502 3\n85 328 1\n87 418 1\n88 443 2\n89 622 1\n90 571 1\n91 571 1\n91 396 1\n93 267 1\n94 446 1\n96 105 4\n96 567 2\n97 588 2\n98 108 1\n98 198 1\n98 586 1\n98 612 1\n99 281 1\n99 604 1\n99 204 1\n100 544 1\n100 491 1\n101 621 1\n102 575 1\n102 245 1\n102 371 1\n103 389 2\n103 623 1\n103 532 4\n103 458 1\n103 107 3\n103 209 3\n103 378 3\n104 376 1\n105 567 1\n106 571 1\n107 314 1\n107 532 4\n107 186 1\n107 209 6\n107 378 3\n111 594 1\n111 127 1\n111 380 1\n111 234 1\n111 519 1\n112 451 2\n112 526 1\n113 571 1\n113 351 1\n113 424 1\n113 400 1\n114 547 1\n115 143 1\n115 339 2\n116 374 1\n116 560 2\n116 224 3\n116 284 3\n116 344 2\n116 619 3\n117 525 1\n117 176 1\n118 333 1\n118 462 1\n118 566 1\n119 349 1\n119 228 1\n119 534 1\n120 277 1\n120 309 1\n121 175 1\n122 544 1\n122 606 1\n122 485 1\n122 465 1\n122 473 2\n122 585 1\n122 550 1\n122 259 1\n122 484 2\n122 144 3\n122 279 1\n122 261 1\n122 232 1\n122 415 1\n123 428 1\n123 563 1\n124 253 1\n125 557 1\n125 143 2\n126 256 2\n126 171 1\n126 555 1\n127 380 1\n127 234 1\n127 519 1\n127 594 1\n128 194 1\n128 554 1\n129 292 1\n129 472 1\n129 435 1\n130 264 1\n130 171 1\n130 555 1\n131 401 2\n131 134 2\n131 538 1\n131 454 1\n131 160 1\n132 529 1\n133 603 1\n133 185 1\n134 401 2\n134 160 1\n134 538 1\n134 454 1\n135 513 1\n136 231 1\n136 474 1\n137 515 1\n137 434 1\n138 571 3\n138 360 2\n138 169 1\n138 496 3\n138 142 3\n138 152 2\n138 574 1\n139 489 1\n139 291 1\n139 250 1\n140 591 1\n140 164 2\n141 489 1\n141 551 1\n142 571 4\n142 360 3\n142 169 2\n142 496 3\n142 574 1\n142 152 3\n143 557 1\n143 186 1\n143 532 1\n144 544 1\n144 585 1\n144 550 1\n144 259 1\n144 261 1\n144 485 1\n144 415 1\n145 254 2\n146 444 1\n146 629 1\n146 520 1\n147 354 6\n147 239 6\n147 257 1\n147 501 1\n148 230 1\n148 366 2\n148 607 1\n148 440 1\n149 451 3\n150 172 3\n150 546 1\n150 514 1\n150 251 1\n150 158 2\n151 340 1\n152 571 4\n152 360 2\n152 169 2\n152 496 2\n152 574 1\n153 357 1\n153 270 1\n154 433 1\n154 362 1\n154 527 1\n154 323 1\n155 232 1\n155 626 1\n155 385 1\n155 238 1\n157 327 1\n158 172 2\n158 546 1\n159 442 1\n160 401 1\n160 454 1\n161 285 1\n162 410 1\n162 545 1\n163 390 2\n164 390 2\n164 591 1\n165 227 1\n165 187 1\n165 263 1\n166 578 2\n166 246 1\n167 191 1\n168 633 1\n168 536 1\n168 223 1\n169 571 2\n169 360 2\n169 496 1\n171 264 4\n171 555 7\n171 599 2\n171 611 1\n171 425 1\n172 546 1\n172 514 1\n172 251 1\n173 447 1\n173 237 1\n173 582 1\n173 278 1\n173 300 1\n173 573 1\n173 617 5\n173 614 1\n174 193 1\n174 441 2\n174 282 2\n174 478 2\n174 439 1\n174 262 1\n176 525 1\n177 477 1\n177 517 3\n178 217 1\n179 564 2\n180 211 1\n180 532 1\n182 326 2\n182 286 1\n183 507 1\n184 225 1\n184 530 1\n184 498 1\n184 597 1\n184 459 1\n185 603 1\n186 532 2\n186 616 1\n186 209 1\n187 227 1\n187 297 1\n187 263 2\n187 379 1\n188 337 2\n188 299 2\n188 583 1\n188 542 1\n188 509 2\n188 311 1\n189 423 1\n190 295 1\n190 288 1\n191 482 1\n192 571 1\n194 554 1\n195 412 1\n195 258 1\n195 628 1\n195 330 1\n196 535 1\n196 244 1\n197 571 1\n197 618 1\n197 360 1\n198 586 1\n198 612 1\n199 247 1\n201 445 2\n201 356 2\n201 251 2\n201 514 2\n202 598 1\n202 328 1\n202 502 1\n203 526 1\n204 281 1\n204 604 1\n206 537 1\n206 233 1\n207 315 1\n208 225 1\n208 549 1\n209 314 1\n209 532 3\n209 378 3\n210 429 1\n211 532 1\n213 416 1\n213 610 1\n213 229 1\n214 571 1\n214 260 1\n215 294 1\n215 394 1\n216 526 1\n218 368 7\n218 406 3\n218 252 3\n218 532 1\n219 296 1\n219 391 1\n220 460 1\n220 329 1\n221 541 1\n222 451 1\n223 536 1\n223 255 1\n223 452 1\n223 342 1\n223 633 1\n224 374 1\n224 560 2\n224 284 3\n224 344 2\n224 619 3\n225 232 1\n225 524 1\n225 549 2\n225 240 3\n225 459 2\n225 511 1\n225 377 1\n226 632 1\n226 562 1\n227 263 1\n228 592 1\n228 401 6\n228 349 6\n228 503 2\n228 534 7\n229 610 1\n230 607 1\n230 366 1\n230 440 1\n231 474 1\n232 385 1\n232 377 1\n232 240 1\n233 537 2\n234 380 1\n234 519 1\n234 594 1\n235 314 1\n236 451 3\n236 608 1\n237 300 1\n237 617 1\n237 614 1\n239 354 6\n239 257 1\n239 501 1\n240 377 1\n240 459 1\n241 543 1\n241 595 1\n242 434 1\n242 532 1\n243 522 1\n243 293 1\n244 535 1\n245 575 1\n245 371 1\n246 353 3\n246 601 1\n246 578 4\n246 600 1\n247 332 1\n247 280 1\n249 320 1\n250 489 1\n250 291 1\n251 356 2\n251 514 3\n251 445 2\n252 368 3\n252 406 3\n257 354 1\n258 412 1\n258 628 1\n258 330 1\n259 544 1\n260 571 1\n261 485 1\n262 282 1\n262 439 1\n263 297 1\n263 381 1\n263 287 1\n263 379 1\n264 570 1\n264 555 4\n264 599 2\n264 611 1\n265 505 1\n266 504 2\n266 479 1\n266 556 1\n266 370 1\n269 486 2\n269 598 1\n269 502 1\n270 357 1\n271 517 1\n271 338 1\n271 272 1\n272 517 1\n272 338 1\n273 593 1\n273 337 1\n274 335 1\n274 436 1\n276 571 2\n276 531 2\n276 308 2\n276 346 2\n276 310 2\n276 584 2\n276 572 2\n276 340 2\n276 385 1\n276 618 2\n276 387 1\n276 526 1\n276 605 1\n276 432 1\n276 396 2\n277 309 1\n278 617 1\n279 465 1\n279 606 1\n280 332 1\n281 516 1\n281 609 1\n281 469 1\n281 604 1\n281 437 1\n282 438 1\n282 439 1\n283 314 1\n284 374 1\n284 560 2\n284 344 2\n284 619 3\n287 381 1\n288 295 1\n289 621 2\n289 393 1\n291 489 1\n292 472 1\n292 435 1\n293 522 1\n296 391 1\n297 379 1\n299 337 1\n299 509 1\n300 617 1\n300 614 1\n301 306 1\n301 319 1\n301 384 1\n305 411 1\n306 319 1\n306 384 1\n307 329 1\n307 532 1\n308 571 2\n308 531 2\n308 310 2\n308 584 2\n308 572 2\n308 618 2\n308 396 2\n310 571 2\n310 531 2\n310 584 2\n310 572 2\n310 618 2\n310 396 2\n311 583 1\n311 337 1\n311 509 1\n312 313 1\n313 630 1\n314 429 1\n314 532 3\n314 565 1\n316 502 4\n316 449 4\n316 598 3\n317 445 2\n318 345 1\n319 384 1\n322 361 1\n322 461 1\n323 433 1\n323 362 1\n323 527 1\n324 451 1\n324 497 1\n325 471 1\n325 399 1\n325 428 1\n327 587 1\n328 502 1\n328 598 1\n329 460 2\n330 596 1\n330 407 1\n330 628 1\n330 412 1\n330 470 1\n330 414 1\n330 613 1\n331 590 1\n333 462 1\n335 436 1\n336 365 1\n336 359 1\n337 593 1\n337 450 1\n337 625 1\n337 409 1\n337 509 1\n337 510 1\n337 583 1\n337 493 1\n337 523 1\n338 517 1\n340 387 1\n340 620 1\n341 451 1\n341 533 1\n341 442 1\n342 452 2\n342 428 1\n343 590 1\n343 463 1\n344 560 2\n344 619 2\n347 558 1\n347 352 1\n347 571 1\n349 592 1\n349 401 5\n349 503 2\n349 534 6\n351 571 1\n351 424 1\n351 400 1\n352 558 1\n352 571 1\n353 578 3\n353 601 1\n353 600 1\n354 501 1\n356 445 2\n356 514 2\n358 521 1\n359 365 1\n360 571 4\n360 496 2\n360 618 1\n361 461 1\n362 433 1\n362 527 1\n363 495 1\n364 561 1\n364 500 1\n366 607 1\n366 440 1\n367 561 1\n368 406 3\n369 371 1\n370 479 1\n370 556 1\n370 504 1\n371 575 1\n373 451 1\n374 619 1\n375 578 1\n376 576 1\n376 634 1\n380 519 1\n380 594 1\n386 451 1\n386 492 1\n386 413 1\n386 467 1\n388 408 1\n388 532 1\n389 458 1\n390 438 8\n390 629 1\n390 419 1\n393 621 1\n396 571 3\n396 584 2\n396 572 2\n396 531 2\n396 618 2\n398 455 1\n399 471 1\n399 428 2\n400 571 1\n400 424 1\n401 538 1\n401 454 1\n401 534 6\n401 592 1\n401 503 2\n402 625 1\n402 409 1\n402 510 1\n404 629 1\n404 569 1\n405 474 1\n407 596 1\n407 613 1\n408 532 1\n409 450 1\n409 625 2\n409 510 2\n409 589 1\n410 545 1\n412 628 1\n413 451 1\n413 467 1\n413 492 1\n414 470 1\n415 585 1\n415 550 1\n417 466 1\n419 438 1\n420 430 1\n421 578 1\n422 577 1\n424 571 9\n424 581 6\n425 555 1\n426 532 1\n426 475 1\n427 480 1\n428 563 1\n428 471 1\n429 565 1\n431 579 1\n431 624 1\n433 527 1\n434 515 1\n434 476 1\n434 532 1\n435 472 1\n437 516 1\n437 609 1\n437 469 1\n438 629 1\n440 607 1\n441 478 2\n442 451 1\n442 627 2\n442 602 1\n444 629 1\n444 520 1\n445 514 2\n447 617 1\n449 502 4\n449 598 3\n450 625 1\n450 510 1\n451 483 1\n451 467 1\n451 532 1\n451 526 3\n451 492 1\n451 497 2\n451 568 1\n452 546 1\n456 615 1\n458 488 1\n465 606 1\n467 492 1\n468 532 3\n469 516 1\n469 609 1\n473 484 6\n477 517 1\n479 504 1\n479 556 1\n487 543 1\n487 595 1\n489 551 1\n490 512 1\n491 544 1\n494 499 1\n496 571 3\n496 574 1\n500 561 1\n502 598 5\n503 534 2\n504 556 1\n508 631 1\n509 583 1\n510 625 2\n511 524 1\n511 549 1\n516 609 1\n518 553 1\n519 594 1\n520 629 1\n524 549 1\n530 597 2\n531 571 3\n531 558 1\n531 572 2\n531 618 3\n531 584 2\n534 592 1\n536 633 1\n539 620 1\n543 595 2\n543 552 1\n548 578 1\n550 585 1\n555 599 2\n555 611 1\n558 571 2\n558 618 1\n560 619 2\n562 632 1\n569 629 1\n571 584 2\n571 572 2\n571 574 1\n571 581 6\n571 618 4\n572 584 2\n572 618 2\n573 617 1\n573 582 1\n578 600 1\n578 601 1\n579 624 1\n582 617 1\n584 618 2\n586 612 1\n596 613 1\n599 611 1\n600 601 1\n614 617 1\n"
  },
  {
    "path": "linkpred/__init__.py",
    "content": "\"\"\"linkpred, a Python package for link prediction\"\"\"\nfrom .linkpred import *\n\n__version__ = \"0.6\"\n"
  },
  {
    "path": "linkpred/cli.py",
    "content": "\"\"\"CLI handling\"\"\"\nimport argparse\nimport json\nimport logging\nimport sys\n\nimport yaml\n\nfrom .exceptions import LinkPredError\nfrom .linkpred import LinkPred\nfrom .predictors import all_predictors\n\nlog = logging.getLogger(\"linkpred\")\n\n__all__ = [\"load_profile\", \"get_config\", \"handle_arguments\"]\n\n\ndef setup_logger():\n    streamhandler = logging.StreamHandler(sys.stdout)\n    formatter = logging.Formatter(\n        \"%(asctime)s - %(levelname)s - %(message)s\", \"%H:%M:%S\"\n    )\n    streamhandler.setFormatter(formatter)\n    log.addHandler(streamhandler)\n\n\ndef load_profile(fname):\n    \"\"\"Load the JSON or YAML profile with the given filename\"\"\"\n    try:\n        with open(fname) as f:\n            if fname.endswith((\".yaml\", \".yml\")):\n                return yaml.safe_load(f)\n            return json.load(f)\n    except Exception as err:\n        msg = f\"Encountered error while loading profile '{fname}'. \"\n        raise LinkPredError(msg) from err\n\n\ndef get_config(args=None):\n    \"\"\"Get configuration as supplied by the user\n\n    If a YAML-or JSON-based profile is supplied, any settinsg therein take\n    priority over command-line arguments.\n\n    \"\"\"\n    args = handle_arguments(args)\n\n    profile = args.pop(\"profile\")\n\n    config = {}\n    predictorlist = [{\"name\": predictor} for predictor in args.pop(\"predictors\")]\n    config[\"predictors\"] = predictorlist\n    config.update(args)\n\n    if profile:\n        config.update(load_profile(profile))\n\n    return config\n\n\ndef handle_arguments(args=None):\n    \"\"\"Get nice CLI interface and return arguments.\"\"\"\n\n    parser = argparse.ArgumentParser(\n        description=\"Easy link prediction tool\",\n        usage=\"%(prog)s training-file [test-file] [options]\",\n    )\n\n    group = parser.add_mutually_exclusive_group()\n    group.add_argument(\n        \"--debug\",\n        action=\"store_true\",\n        dest=\"debug\",\n        default=False,\n        help=\"Show debug messages\",\n    )\n    group.add_argument(\n        \"-q\",\n        \"--quiet\",\n        action=\"store_true\",\n        dest=\"quiet\",\n        default=False,\n        help=\"Don't show info messages\",\n    )\n\n    # TODO allow case-insensitive match\n    output_types = [\n        \"recall-precision\",\n        \"f-score\",\n        \"roc\",\n        \"cache-predictions\",\n        \"cache-evaluations\",\n        \"fmax\",\n    ]\n    output_help = (\n        \"Type of output(s) to produce (default: recall-precision). \"\n        \"Allowed values are: \" + \", \".join(output_types)\n    )\n    parser.add_argument(\n        \"-o\",\n        \"--output\",\n        help=output_help,\n        nargs=\"*\",\n        choices=output_types,\n        default=output_types[0:1],\n        metavar=\"OUTPUT\",\n    )\n\n    # TODO allow case-insensitive match\n    parser.add_argument(\n        \"-f\",\n        \"--chart-filetype\",\n        default=\"pdf\",\n        help=\"File type for charts (default: %(default)s)\",\n    )\n\n    parser.add_argument(\n        \"-i\",\n        \"--no-interpolation\",\n        dest=\"interpolation\",\n        help=\"Do not interpolate recall-precision charts\",\n        action=\"store_false\",\n        default=True,\n    )\n\n    # TODO allow case-insensitive match\n    predictors = [p.__name__ for p in all_predictors()]\n    predictor_help = (\n        \"Predictor(s) to use for link prediction. \"\n        \"Allowed values are: \" + \", \".join(predictors)\n    )\n    parser.add_argument(\n        \"-p\",\n        \"--predictors\",\n        nargs=\"*\",\n        choices=predictors,\n        default=[],\n        help=predictor_help,\n        metavar=\"PREDICTOR\",\n    )\n\n    all_help = \"Predict all links, including ones present in the training network\"\n    parser.add_argument(\n        \"-a\",\n        \"--all\",\n        action=\"store_const\",\n        dest=\"exclude\",\n        const=\"\",\n        default=\"old\",\n        help=all_help,\n    )\n\n    parser.add_argument(\"-P\", \"--profile\", help=\"JSON/YAML profile file\")\n\n    parser.add_argument(\"training-file\", help=\"File with the training network\")\n    parser.add_argument(\"test-file\", nargs=\"?\", help=\"File with the test network\")\n\n    results = parser.parse_args(args)\n\n    if results.debug:\n        log.setLevel(logging.DEBUG)\n    elif results.quiet:\n        log.setLevel(logging.WARNING)\n    else:\n        log.setLevel(logging.INFO)\n\n    # Return as plain dictionary\n    return vars(results)\n\n\ndef main(args=None):\n    \"\"\"Main function\n\n    This gets called if one invokes linkpred from the command-line\n    \"\"\"\n    config = get_config(args)\n    setup_logger()\n    linkpred = LinkPred(config)\n    linkpred.preprocess()\n    linkpred.predict_all()\n    linkpred.setup_output()\n    linkpred.process_predictions()\n"
  },
  {
    "path": "linkpred/evaluation/__init__.py",
    "content": "\"\"\"Module for evaluating link prediction results\"\"\"\nfrom .scoresheet import *\nfrom .static import *\n"
  },
  {
    "path": "linkpred/evaluation/listeners.py",
    "content": "import copy\nimport logging\nfrom time import localtime, strftime\n\nimport smokesignal\n\nfrom ..util import interpolate\nfrom .static import EvaluationSheet\n\nlog = logging.getLogger(__name__)\n\n__all__ = [\n    \"EvaluatingListener\",\n    \"CachePredictionListener\",\n    \"Listener\",\n    \"Plotter\",\n    \"CacheEvaluationListener\",\n    \"FMaxListener\",\n    \"RecallPrecisionPlotter\",\n    \"FScorePlotter\",\n    \"ROCPlotter\",\n    \"PrecisionAtKListener\",\n    \"MarkednessPlotter\",\n]\n\n\ndef _timestamped_filename(basename, ext=\"txt\"):\n    return basename + strftime(\"_%Y-%m-%d_%H.%M.\", localtime()) + ext\n\n\nclass Listener:\n    def __init__(self):\n        smokesignal.on(\"dataset_finished\", self.on_dataset_finished)\n        smokesignal.on(\"run_finished\", self.on_run_finished)\n\n    def on_dataset_finished(self, dataset):\n        pass\n\n    def on_run_finished(self):\n        pass\n\n\nclass EvaluatingListener(Listener):\n    def __init__(self, **kwargs):\n        smokesignal.on(\"prediction_finished\", self.on_prediction_finished)\n        self.params = kwargs\n\n        super().__init__()\n\n    def on_prediction_finished(self, scoresheet, dataset, predictor):\n        evaluation = EvaluationSheet(scoresheet, **self.params)\n        smokesignal.emit(\n            \"evaluation_finished\",\n            evaluation=evaluation,\n            dataset=dataset,\n            predictor=predictor,\n        )\n\n\nclass CachePredictionListener(Listener):\n    def __init__(self):\n        smokesignal.on(\"prediction_finished\", self.on_prediction_finished)\n        super().__init__()\n        self.encoding = \"utf-8\"\n\n    def on_prediction_finished(self, scoresheet, dataset, predictor):\n        self.fname = _timestamped_filename(f\"{dataset}-{predictor}-predictions\")\n        scoresheet.to_file(self.fname)\n\n\nclass CacheEvaluationListener(Listener):\n    def __init__(self):\n        smokesignal.on(\"evaluation_finished\", self.on_evaluation_finished)\n        super().__init__()\n\n    def on_evaluation_finished(self, evaluation, dataset, predictor):\n        self.fname = _timestamped_filename(f\"{dataset}-{predictor}-predictions\")\n        evaluation.to_file(self.fname)\n\n\nclass FMaxListener(Listener):\n    def __init__(self, name, beta=1):\n        self.beta = beta\n        self.fname = _timestamped_filename(\"%s-Fmax\" % name)\n\n        smokesignal.on(\"evaluation_finished\", self.on_evaluation_finished)\n        super().__init__()\n\n    def on_evaluation_finished(self, evaluation, dataset, predictor):\n        fmax = evaluation.f_score(self.beta).max()\n\n        status = f\"{dataset}\\t{predictor}\\t{fmax:.4f}\\n\"\n\n        with open(self.fname, \"a\") as f:\n            f.write(status)\n        log.info(\"Evaluation finished: %s\", status)\n\n\nclass PrecisionAtKListener(Listener):\n    def __init__(self, name, k=10):\n        self.k = k\n        self.fname = _timestamped_filename(\"%s-precision-at-%d\" % (name, self.k))\n\n        smokesignal.on(\"evaluation_finished\", self.on_evaluation_finished)\n        super().__init__()\n\n    def on_evaluation_finished(self, evaluation, dataset, predictor):\n        precision = evaluation.precision()[self.k]\n\n        status = f\"{dataset}\\t{predictor}\\t{precision:.4f}\\n\"\n        with open(self.fname, \"a\") as f:\n            f.write(status)\n        log.info(\"Evaluation finished: %s\", status)\n\n\nGENERIC_CHART_LOOKS = [\n    \"k-\",\n    \"k--\",\n    \"k.-\",\n    \"k:\",\n    \"r-\",\n    \"r--\",\n    \"r.-\",\n    \"r:\",\n    \"b-\",\n    \"b--\",\n    \"b.-\",\n    \"b:\",\n    \"g-\",\n    \"g--\",\n    \"g.-\",\n    \"g:\",\n    \"c-\",\n    \"c--\",\n    \"c.-\",\n    \"c:\",\n    \"y-\",\n    \"y--\",\n    \"y.-\",\n    \"y:\",\n]\n\n\nclass Plotter(Listener):\n    def __init__(self, name, xlabel=\"\", ylabel=\"\", filetype=\"pdf\", chart_looks=None):\n        import matplotlib.pyplot as plt\n\n        self.name = name\n        self.filetype = filetype\n        self.chart_looks = chart_looks\n        self._charttype = \"\"\n        self._legend_props = {\"prop\": {\"size\": \"x-small\"}}\n        self.fig = plt.figure()\n        ax = self.fig.add_axes([0.1, 0.1, 0.8, 0.8], xlabel=xlabel, ylabel=ylabel)\n        ax.set_ylim((0, 1))\n        self._x = []\n        self._y = []\n\n        smokesignal.on(\"evaluation_finished\", self.on_evaluation_finished)\n        super().__init__()\n\n    def add_line(self, predictor=\"\"):\n        ax = self.fig.axes[0]\n        ax.plot(self._x, self._y, self.chart_look(), label=predictor)\n\n        log.debug(\n            \"Added line with %d points: start = (%.2f, %.2f), end = (%.2f, %.2f)\",\n            len(self._x),\n            self._x[0],\n            self._y[0],\n            self._x[-1],\n            self._y[-1],\n        )\n\n    def chart_look(self, default=None):\n        if not self.chart_looks:\n            if not default:\n                default = GENERIC_CHART_LOOKS\n            self.chart_looks = copy.copy(default)\n        return self.chart_looks.pop(0)\n\n    def on_evaluation_finished(self, evaluation, dataset, predictor):\n        self.setup_coords(evaluation)\n        self.add_line(predictor)\n\n    def on_run_finished(self):\n        # Fix looks\n        for ax in self.fig.axes:\n            ax.legend(**self._legend_props)\n\n        # Save to file\n        self.fname = _timestamped_filename(\n            f\"{self.name}-{self._charttype}\", self.filetype\n        )\n        self.fig.savefig(self.fname)\n\n\nclass RecallPrecisionPlotter(Plotter):\n    def __init__(\n        self, name, xlabel=\"Recall\", ylabel=\"Precision\", *, interpolation=True, **kwargs\n    ):\n        super().__init__(name, xlabel, ylabel, **kwargs)\n        self._charttype = \"recall-precision\"\n        self.interpolation = interpolation\n\n    def add_line(self, predictor=\"\"):\n        if self.interpolation:\n            self._y = interpolate(self._y)\n        Plotter.add_line(self, predictor)\n\n    def setup_coords(self, evaluation):\n        self._x = evaluation.recall()\n        self._y = evaluation.precision()\n\n\nclass FScorePlotter(Plotter):\n    def __init__(self, name, xlabel=\"#\", ylabel=\"F-score\", beta=1, **kwargs):\n        super().__init__(name, xlabel, ylabel, **kwargs)\n        self._charttype = \"F-Score\"\n        self.beta = beta\n\n    def setup_coords(self, evaluation):\n        self._x = range(len(evaluation))\n        self._y = evaluation.f_score(self.beta)\n\n\nclass ROCPlotter(Plotter):\n    def __init__(\n        self, name, xlabel=\"False pos. rate\", ylabel=\"True pos. rate\", **kwargs\n    ):\n        super().__init__(name, xlabel, ylabel, **kwargs)\n        self._charttype = \"ROC\"\n\n    def setup_coords(self, evaluation):\n        self._x = evaluation.fallout()\n        self._y = evaluation.recall()\n\n\nclass MarkednessPlotter(Plotter):\n    def __init__(self, name, xlabel=\"Miss\", ylabel=\"Precision\", **kwargs):\n        super().__init__(name, xlabel, ylabel, **kwargs)\n        self._charttype = \"Markedness\"\n        self._legend_props[\"loc\"] = \"upper left\"\n\n    def setup_coords(self, evaluation):\n        self._x = evaluation.miss()\n        self._y = evaluation.precision()\n"
  },
  {
    "path": "linkpred/evaluation/scoresheet.py",
    "content": "import logging\nfrom collections import defaultdict\n\nimport networkx as nx\nfrom networkx.readwrite.pajek import make_qstr\n\nlog = logging.getLogger(__name__)\n__all__ = [\"Pair\", \"BaseScoresheet\", \"Scoresheet\"]\n\n\nclass BaseScoresheet(defaultdict):\n    \"\"\"Score sheet for evaluation of IR and similar\n\n    This is a simple dict-like object, whose values are numeric\n    (floats). It adds the methods `ranked_items` and `top`.\n\n    Example\n    -------\n    >>> data = {('a', 'b'): 0.8, ('b', 'c'): 0.5, ('c', 'a'): 0.2}\n    >>> sheet = Scoresheet(data)\n    >>> for (x, y), score in sheet.ranked_items():\n    ...     print(\"{}-{}: {}\".format(x, y, score))\n    b-a: 0.8\n    c-b: 0.5\n    c-a: 0.2\n\n    \"\"\"\n\n    def __init__(self, data=None):\n        defaultdict.__init__(self, float)\n        if data:\n            self.update(self.process_data(data))\n\n    def __setitem__(self, key, val):\n        dict.__setitem__(self, key, float(val))\n\n    def process_data(self, data):\n        \"\"\"Can be overridden by child classes\"\"\"\n        return data\n\n    def ranked_items(self, threshold=None):\n        \"\"\"Return items in decreasing order of their score\n\n        Arguments\n        ---------\n        threshold : int\n            Maximum number of items to return (in total)\n\n        Returns\n        -------\n        (item, score) : tuple of item and score\n\n        \"\"\"\n        threshold = threshold or len(self)\n        log.debug(\"Called Scoresheet.ranked_items(): threshold=%d\", threshold)\n\n        # Sort first by score, then by key. This way, we always get the same\n        # ranking, even in case of ties.\n        # We use the tmp structure because it is much faster than\n        # itemgetter(1, 0).\n        tmp = ((score, key) for key, score in self.items())\n        ranked_data = sorted(tmp, reverse=True)\n\n        for score, key in ranked_data[:threshold]:\n            yield key, score\n\n    def top(self, n=10):\n        return dict(self.ranked_items(threshold=n))\n\n    @staticmethod\n    def from_record(line, delimiter=\"\\t\"):\n        line = line.rstrip(\"\\n\")\n        return line.rstrip(\"\\n\").split(delimiter)\n\n    @staticmethod\n    def to_record(key, value, delimiter=\"\\t\"):\n        key, value = map(make_qstr, (key, value))\n        return f\"{key}{delimiter}{value}\\n\"\n\n    @classmethod\n    def from_file(cls, fname, delimiter=\"\\t\", encoding=\"utf-8\"):\n        \"\"\"Create new instance from CSV file *fname*\"\"\"\n        d = cls()\n        with open(fname, \"rb\") as fh:\n            for line in fh:\n                key, score = cls.from_record(line.decode(encoding), delimiter)\n                d[key] = score\n        return d\n\n    def to_file(self, fname, delimiter=\"\\t\", encoding=\"utf-8\"):\n        \"\"\"Save to CSV file *fname*\"\"\"\n        with open(fname, \"wb\") as fh:\n            for key, score in self.ranked_items():\n                fh.write(self.to_record(key, score, delimiter).encode(encoding))\n\n\nclass Pair:\n    \"\"\"An unsorted pair of things.\n\n    We could probably also use frozenset for this, but a Pair class opens\n    possibilities for the future, such as extensions to 'directed' pairs\n    (where the order is important) or to self-loops (where the two elements\n    are the same).\n\n    Example\n    -------\n    >>> t = ('a', 'b')\n    >>> Pair(t) == Pair(*t) == Pair('b', 'a')\n    True\n\n    \"\"\"\n\n    def __init__(self, *args):\n        if len(args) == 1:\n            key = args[0]\n            if isinstance(key, Pair):\n                a, b = key.elements\n            elif isinstance(key, tuple) and len(key) == 2:\n                a, b = key\n            else:\n                raise TypeError(\"Key '%s' is not a Pair or tuple.\" % (key))\n        elif len(args) == 2:\n            a, b = args\n        else:\n            msg = \"__init__() takes 1 or 2 arguments in addition to self\"\n            raise TypeError(msg)\n        # For link prediction, a and b are two different nodes\n        assert a != b, f\"Predicted link ({a}, {b}) is a self-loop!\"\n        self.elements = self._sorted_tuple((a, b))\n\n    @staticmethod\n    def _sorted_tuple(t):\n        a, b = t\n        try:\n            return (a, b) if a > b else (b, a)\n        except TypeError:\n            # Different node types. This does not hande all possible edge\n            # cases but should be enough for most real-world scenarios.\n            return (a, b) if str(a) > str(b) else (b, a)\n\n    def __eq__(self, other):\n        try:\n            return self.elements == other.elements\n        except AttributeError:\n            return self.elements == self._sorted_tuple(other)\n\n    def __ne__(self, other):\n        return not self == other\n\n    def __lt__(self, other):\n        try:\n            return self.elements < other.elements\n        except AttributeError:\n            return self.elements < self._sorted_tuple(other)\n\n    def __gt__(self, other):\n        try:\n            return self.elements > other.elements\n        except AttributeError:\n            return self.elements > self._sorted_tuple(other)\n\n    def __le__(self, other):\n        return self < other or self == other\n\n    def __ge__(self, other):\n        return self > other or self == other\n\n    def __getitem__(self, idx):\n        return self.elements[idx]\n\n    def __hash__(self):\n        return hash(self.elements)\n\n    def __str__(self):\n        return \"{} - {}\".format(*self.elements)\n\n    def __repr__(self):\n        return \"Pair%s\" % repr(self.elements)\n\n    def __iter__(self):\n        return iter(self.elements)\n\n    def __len__(self):\n        return len(self.elements)\n\n\nclass Scoresheet(BaseScoresheet):\n    \"\"\"Scoresheet for link prediction\n\n    Scoresheet's keys are always Pairs.\n\n    \"\"\"\n\n    def __getitem__(self, key):\n        return BaseScoresheet.__getitem__(self, Pair(key))\n\n    def __setitem__(self, key, val):\n        BaseScoresheet.__setitem__(self, Pair(key), float(val))\n\n    def __delitem__(self, key):\n        return dict.__delitem__(self, Pair(key))\n\n    def process_data(self, data, weight=\"weight\"):\n        if isinstance(data, dict):\n            return {Pair(k): float(v) for k, v in data.items()}\n        if isinstance(data, nx.Graph):\n            return {Pair(u, v): float(d[weight]) for u, v, d in data.edges(data=True)}\n        # We assume that data is some sort of iterable, like a list or tuple\n        return {Pair(k): float(v) for k, v in data}\n\n    @staticmethod\n    def from_record(line, delimiter=\"\\t\"):\n        u, v, score = line.rstrip(\"\\n\").split(delimiter)\n        return (u, v), score\n\n    @staticmethod\n    def to_record(key, value, delimiter=\"\\t\"):\n        u, v = key\n        u, v, score = map(make_qstr, (u, v, value))\n        return f\"{u}{delimiter}{v}{delimiter}{score}\\n\"\n"
  },
  {
    "path": "linkpred/evaluation/static.py",
    "content": "import logging\n\nimport numpy as np\n\nfrom .scoresheet import BaseScoresheet\n\nlog = logging.getLogger(__name__)\n\n__all__ = [\"EvaluationSheet\", \"StaticEvaluation\", \"UndefinedError\"]\n\n\nclass UndefinedError(Exception):\n    \"\"\"Raised when the method's result is undefined\"\"\"\n\n\nclass StaticEvaluation:\n    \"\"\"Static evaluation of IR\"\"\"\n\n    def __init__(self, retrieved=None, relevant=None, universe=None):\n        \"\"\"\n        Initialize IR evaluation.\n\n        We determine the following table:\n\n        +--------------+---------------+\n        | tp           | fp            |\n        | ret & rel    | ret & ~rel    |\n        +--------------+---------------+\n        | fn           | tn            |\n        | ~ret & rel   | ~ret & ~rel   |\n        +--------------+---------------+\n\n        Arguments\n        ---------\n        retrieved : a list or set\n            iterable of the retrieved items\n\n        relevant : a list or set\n            iterable of the relevant items\n\n        universe : a list or set, an int or None\n            If universe is an iterable, it is interpreted as the set of all\n            items in the system. If universe is an int, it is interpreted as\n            the *number* of items in the system. This allows for fewer checks\n            but is more memory-efficient. If universe is None, it is supposed\n            to be unknown. This still allows for some measures, including\n            precision and recall, to be calculated.\n\n        \"\"\"\n        retrieved = set(retrieved) if retrieved else set()\n        relevant = set(relevant) if relevant else set()\n\n        self.fp = retrieved - relevant\n        self.fn = relevant - retrieved\n        self.tp = retrieved & relevant\n        if universe is None:\n            self.tn = None\n            self.num_universe = -1\n        elif isinstance(universe, int):\n            self.tn = None\n            self.num_universe = universe\n            if len(retrieved) > self.num_universe or len(relevant) > self.num_universe:\n                msg = \"Retrieved cannot be larger than universe.\"\n                raise ValueError(msg)\n        else:\n            universe = set(universe)\n            if not (retrieved <= universe and relevant <= universe):\n                msg = \"Retrieved and relevant should be subsets of universe.\"\n                raise ValueError(msg)\n            self.num_universe = len(universe)\n            self.tn = universe - retrieved - relevant\n            del universe\n        self.update_counts()\n\n    def update_counts(self):\n        self.num_fp = len(self.fp)\n        self.num_fn = len(self.fn)\n        self.num_tp = len(self.tp)\n        if self.tn is not None:\n            self.num_tn = len(self.tn)\n        elif self.num_universe == -1:\n            self.num_tn = -1\n        else:\n            self.num_tn = self.num_universe - self.num_fp - self.num_fn - self.num_tp\n            assert self.num_tn >= 0\n\n    def add_retrieved_item(self, item):\n        self.update_retrieved({item})\n\n    def update_retrieved(self, new):\n        new = set(new)\n\n        if not (new.isdisjoint(self.tp) and new.isdisjoint(self.fp)):\n            msg = \"One or more elements in `new` have already been retrieved.\"\n            raise ValueError(msg)\n\n        relevant_new = new & self.fn\n        nonrelevant_new = new - relevant_new\n\n        self.tp |= relevant_new\n        self.fp |= nonrelevant_new\n        if self.tn:\n            if not new <= self.fn | self.tn:\n                msg = (\n                    \"Newly retrieved items should be a subset \"\n                    \"of currently unretrieved items.\"\n                )\n                raise ValueError(msg)\n            self.tn -= nonrelevant_new\n        self.fn -= relevant_new\n        self.update_counts()\n\n\ndef ensure_defined(func):\n    def _wrapper(self, *args, **kwargs):\n        if self.data.shape[0] == 0:\n            msg = \"Measure is undefined if there are no relevant or retrieved items\"\n            raise UndefinedError(msg)\n        return func(self, *args, **kwargs)\n\n    return _wrapper\n\n\ndef ensure_universe_known(func):\n    def _wrapper(self, *args, **kwargs):\n        # If tn is -1 somewhere, we know that universe is not defined.\n        if np.where(self.tn == -1, True, False).any():\n            msg = \"Measure is undefined if universe is unknown\"\n            raise UndefinedError(msg)\n        return func(self, *args, **kwargs)\n\n    return _wrapper\n\n\nclass EvaluationSheet:\n    def __init__(self, data=None, relevant=None, universe=None):\n        if isinstance(data, BaseScoresheet):\n            if relevant is None:\n                msg = (\n                    \"Cannot create evaluation sheet from \"\n                    \"scoresheet without set of relevant items\"\n                )\n                raise TypeError(msg)\n            log.debug(\"Counting for evaluation sheet...\")\n            static = StaticEvaluation(relevant=relevant, universe=universe)\n            # Initialize empty array of right dimensions\n            # 4 columns for tp, fp, fn, tn\n            self.data = np.empty((len(data), 4))\n            for i, (prediction, _) in enumerate(data.ranked_items()):\n                static.add_retrieved_item(prediction)\n                self.data[i] = (\n                    static.num_tp,\n                    static.num_fp,\n                    static.num_fn,\n                    static.num_tn,\n                )\n            log.debug(\"Finished counting evaluation sheet...\")\n        elif isinstance(data, np.ndarray):\n            self.data = data\n        else:\n            msg = f\"Cannot create evaluation sheet from unknown data type {type(data)}.\"\n            raise TypeError(msg)\n\n    def __len__(self):\n        return len(self.data)\n\n    @property\n    def tp(self):\n        return self.data[:, 0]\n\n    @property\n    def fp(self):\n        return self.data[:, 1]\n\n    @property\n    def fn(self):\n        return self.data[:, 2]\n\n    @property\n    def tn(self):\n        return self.data[:, 3]\n\n    def to_file(self, fname, *args, **kwargs):\n        np.savetxt(fname, self.data, *args, **kwargs)\n\n    @classmethod\n    def from_file(cls, fname, *args, **kwargs):\n        data = np.loadtxt(fname, *args, **kwargs)\n        return cls(data)\n\n    @ensure_defined\n    def precision(self):\n        return self.tp / (self.tp + self.fp)\n\n    @ensure_defined\n    def recall(self):\n        return self.tp / (self.tp + self.fn)\n\n    @ensure_defined\n    @ensure_universe_known\n    def fallout(self):\n        return self.fp / (self.fp + self.tn)\n\n    @ensure_defined\n    @ensure_universe_known\n    def miss(self):\n        return self.fn / (self.fn + self.tn)\n\n    @ensure_defined\n    @ensure_universe_known\n    def accuracy(self):\n        return (self.tp + self.tn) / self.data.sum(axis=1)\n\n    @ensure_defined\n    def f_score(self, beta=1):\n        r\"\"\"Compute F-score\n\n        F is the harmonic mean of precision and recall:\n\n            F = 2PR / (P + R)\n\n        We use the generalized form:\n\n            F = (beta^2 + 1)PR / (beta^2 P + R)\n              = (beta^2 + 1)tp / ((beta^2 + 1)tp + beta^2fn + fp)\n\n        The parameter beta allows assigning more weight to precision or recall.\n        If beta > 1, recall is emphasized over precision. If beta < 1,\n        precision is emphasized over recall.\n\n        \"\"\"\n        beta2 = beta**2\n        beta2_tp = (beta2 + 1) * self.tp\n        return beta2_tp / (beta2_tp + beta2 * self.fn + self.fp)\n\n    @ensure_defined\n    @ensure_universe_known\n    def generality(self):\n        \"\"\"Compute generality of the query\n\n        Generality G is defined as:\n\n            G = (tp + fn) / (tp + fn + fp + tp)\n\n        Returns\n        -------\n\n        G : float\n\n        \"\"\"\n        # Return single number: this is constant wrt what is retrieved\n        return ((self.tp + self.fn) / self.data.sum(axis=1))[0]\n"
  },
  {
    "path": "linkpred/exceptions.py",
    "content": "\"\"\"Package-specific exceptions\"\"\"\n\n\nclass LinkPredError(Exception):\n    \"\"\"Link prediction error\"\"\"\n"
  },
  {
    "path": "linkpred/linkpred.py",
    "content": "\"\"\"linkpred main module\"\"\"\nimport contextlib\nimport logging\nimport os\n\nimport networkx as nx\nimport smokesignal\n\nfrom . import predictors\nfrom .evaluation import Pair\nfrom .evaluation import listeners as l\nfrom .exceptions import LinkPredError\nfrom .preprocess import (\n    without_low_degree_nodes,\n    without_selfloops,\n    without_uncommon_nodes,\n)\n\nlog = logging.getLogger(__name__)\n\n__all__ = [\"LinkPred\", \"read_network\"]\n\n\ndef for_comparison(G, exclude=None):\n    \"\"\"Return the result in a format, suitable for comparison.\n\n    In practice this means we return it as a set of Pairs.\n\n    \"\"\"\n    if not exclude:\n        return set(G.edges())\n\n    exclude = {Pair(u, v) for u, v in exclude}\n    return {Pair(u, v) for u, v in G.edges()} - exclude\n\n\ndef pretty_print(name, params=None):\n    \"\"\"Pretty print a predictor name\n\n    Arguments\n    ---------\n    name : string\n        predictor name\n\n    params : dict or None\n        dictionary of parameter name -> value\n\n    \"\"\"\n    if not params:\n        return name\n\n    pretty_params = \", \".join(f\"{k} = {str(v)}\" for k, v in params.items())\n    return f\"{name} ({pretty_params})\"\n\n\ndef _read_pajek(*args, **kwargs):\n    \"\"\"Read Pajek file and make sure that we get an nx.Graph or nx.DiGraph\"\"\"\n    G = nx.read_pajek(*args, **kwargs)\n    edges = G.edges()\n    if len(set(edges)) < len(edges):  # multiple edges\n        log.warning(\"Network contains multiple edges. These will be ignored.\")\n\n    return nx.DiGraph(G) if G.is_directed() else nx.Graph(G)\n\n\nFILETYPE_READERS = {\n    \".net\": _read_pajek,\n    \".gml\": nx.read_gml,\n    \".graphml\": nx.read_graphml,\n    \".gexf\": nx.read_gexf,\n    \".edgelist\": nx.read_edgelist,\n    \".adjlist\": nx.read_adjlist,\n}\n\n\ndef read_network(fh):\n    \"\"\"Read the network file and return as nx.Graph or nx.DiGraph\n\n    Arguments\n    ---------\n    fh : string\n        file handle or file name\n\n    \"\"\"\n    try:\n        fname = fh.name\n    except AttributeError:\n        # fh is a string or path\n        fname = fh\n\n    ext = os.path.splitext(fname.lower())[1]\n    try:\n        read = FILETYPE_READERS[ext]\n        log.info(\"Reading file '%s'...\", fname)\n        network = read(fh)\n        log.info(\"Successfully read file.\")\n    except KeyError as err:\n        msg = (\n            f\"File '{fname}' is of an unknown type. \"\n            f\"Known types are: {', '.join(FILETYPE_READERS)}.\"\n        )\n        raise LinkPredError(msg) from err\n\n    return network\n\n\nclass LinkPred:\n\n    \"\"\"linkpred main object\n\n    LinkPred stores all configuration and provides a high-level interface to\n    most functionality.\n\n    \"\"\"\n\n    def __init__(self, config=None):\n        # default config\n        self.config = {\n            \"chart_filetype\": \"pdf\",\n            \"eligible\": None,\n            \"interpolation\": False,\n            \"label\": \"\",\n            \"min_degree\": 1,\n            \"exclude\": \"old\",\n            \"output\": [\"recall-precision\"],\n            \"predictors\": [],\n            \"test-file\": None,\n            \"training-file\": None,\n        }\n        if config:\n            self.config.update(config)\n        log.debug(\"Config: %s\", self.config)\n\n        if not self.config[\"predictors\"]:\n            msg = \"No predictor specified. Aborting...\"\n            raise LinkPredError(msg)\n\n        self.label = (\n            self.config[\"label\"] or os.path.splitext(self.config[\"training-file\"])[0]\n        )\n        self.training = self.network(\"training-file\")\n        self.test = self.network(\"test-file\")\n        self.evaluator = None\n        self.listeners = []\n\n    @property\n    def excluded(self):\n        \"\"\"Get set of links that should not be predicted\"\"\"\n        exclude = self.config[\"exclude\"]\n        if not exclude:\n            return set()  # No nodes are excluded\n        if exclude == \"old\":\n            return set(self.training.edges())\n        if exclude == \"new\":\n            return set(nx.non_edges(self.training))\n\n        msg = (\n            f\"Value '{exclude}' for exclude is unexpected. Use either 'old', 'new' or \"\n            \"empty string '' (for no exclusions)\"\n        )\n        raise LinkPredError(msg)\n\n    def network(self, key):\n        \"\"\"Get network for given key\"\"\"\n        with contextlib.suppress(KeyError):\n            network_file = self.config[key]\n        if network_file:\n            return read_network(network_file)\n        return None\n\n    def preprocess(self):\n        \"\"\"Preprocess all networks according to configuration\"\"\"\n\n        log.info(\"Starting preprocessing...\")\n\n        def preprocessed(G):\n            return without_low_degree_nodes(\n                without_selfloops(G), minimum=self.config[\"min_degree\"]\n            )\n\n        if self.test:\n            networks = [preprocessed(G) for G in (self.training, self.test)]\n            self.training, self.test = without_uncommon_nodes(networks)\n        else:  # Only a training network\n            self.training = preprocessed(self.training)\n\n        log.info(\"Finished preprocessing.\")\n\n    def setup_output(self):\n        \"\"\"Configure listeners\"\"\"\n        filetype = self.config[\"chart_filetype\"]\n        interpolation = self.config[\"interpolation\"]\n\n        listeners = {\n            \"cache-predictions\": (l.CachePredictionListener, False, {}),\n            \"recall-precision\": (\n                l.RecallPrecisionPlotter,\n                True,\n                {\n                    \"name\": self.label,\n                    \"filetype\": filetype,\n                    \"interpolation\": interpolation,\n                },\n            ),\n            \"f-score\": (\n                l.FScorePlotter,\n                True,\n                {\"name\": self.label, \"filetype\": filetype},\n            ),\n            \"roc\": (l.ROCPlotter, True, {\"name\": self.label, \"filetype\": filetype}),\n            \"fmax\": (l.FMaxListener, True, {\"name\": self.label}),\n            \"cache-evaluations\": (l.CacheEvaluationListener, True, {}),\n        }\n\n        for output in self.config[\"output\"]:\n            name = output.lower()\n            listener, evaluating, kwargs = listeners[name]\n\n            if evaluating:\n                if not self.test:\n                    msg = f\"Cannot evaluate ({output}) without test network\"\n                    raise LinkPredError(msg)\n\n                # Set up an 'evaluator': a listener that routes predictions\n                # and turns them into evaluations\n                if not self.evaluator:\n                    test_set = for_comparison(self.test, exclude=self.excluded)\n                    n = len(self.test)\n                    # Universe = all possible edges, except for the ones that\n                    # we no longer consider because they're excluded\n                    # Make sure we get an int here.\n                    num_universe = n * (n - 1) // 2 - len(self.excluded)\n                    self.evaluator = l.EvaluatingListener(\n                        relevant=test_set, universe=num_universe\n                    )\n\n            self.listeners.append(listener(**kwargs))\n            log.debug(\"Added listener for '%s'\", output)\n\n    def do_predict_all(self):\n        \"\"\"Generator that yields predictions based on training network\n\n        Yields\n        ------\n        (label, scoresheet) : a 2-tuple\n            2-tuple consisting of a string (label of the prediction) and\n            a Scoresheet (actual predictions)\n\n        \"\"\"\n        for predictor_profile in self.config[\"predictors\"]:\n            params = predictor_profile.get(\"parameters\", {})\n            name = predictor_profile[\"name\"]\n            predictor_class = getattr(predictors, name)\n            label = predictor_profile.get(\"displayname\", pretty_print(name, params))\n\n            log.info(\"Executing %s...\", label)\n            predictor = predictor_class(\n                self.training, eligible=self.config[\"eligible\"], excluded=self.excluded\n            )\n            scoresheet = predictor.predict(**params)\n            log.info(\"Finished executing %s.\", label)\n\n            # XXX TODO Do we need name?\n            yield name, scoresheet\n\n    def predict_all(self):\n        \"\"\"Perform all predictions according to configuration\n\n        The predictions are only executed when `process_predictions` is called\n        or when `LinkPred.predictions` is accessed in some other way.\n\n        \"\"\"\n        self.predictions = self.do_predict_all()\n        return self.predictions\n\n    def process_predictions(self):\n        \"\"\"Process (evaluate, log...) all predictions according to config\"\"\"\n\n        # The following loop actually executes the predictors\n        for predictorname, scoresheet in self.predictions:\n            log.debug(\n                \"Predictor '%s' yields %d predictions\", predictorname, len(scoresheet)\n            )\n            smokesignal.emit(\n                \"prediction_finished\",\n                scoresheet=scoresheet,\n                dataset=self.label,\n                predictor=predictorname,\n            )\n\n        smokesignal.emit(\"dataset_finished\", dataset=self.label)\n        smokesignal.emit(\"run_finished\")\n        log.info(\"Prediction run finished\")\n"
  },
  {
    "path": "linkpred/network/__init__.py",
    "content": "from .addremove import *\nfrom .algorithms import *\n"
  },
  {
    "path": "linkpred/network/addremove.py",
    "content": "import logging\nimport random\n\nimport networkx as nx\n\nlog = logging.getLogger(__name__)\n\n__all__ = [\"add_random_edges\", \"remove_random_edges\", \"add_remove_random_edges\"]\n\n\ndef assert_is_percentage(pct):\n    if not 0 <= pct <= 1:\n        msg = \"Percentage should be float between 0 and 1\"\n        raise ValueError(msg)\n\n\ndef add_random_edges(G, pct):\n    \"\"\"Add `n` random edges to G (`n` = fraction of current edge count)\n\n    Parameters\n    ----------\n    G : a networkx.Graph\n        the network\n\n    pct : float\n        A percentage (between 0 and 1)\n    \"\"\"\n    assert_is_percentage(pct)\n    m = G.size()\n    to_add = int(m * pct)\n    log.debug(\"Will add %d edges to %d (%f)\", to_add, m, pct)\n\n    new_edges = set(nx.non_edges(G))\n    G.add_edges_from(random.sample(list(new_edges), to_add), weight=1)\n\n\ndef remove_random_edges(G, pct):\n    \"\"\"Randomly remove `n` edges from G (`n` = fraction of current edge count)\n\n    Parameters\n    ----------\n    G : a networkx.Graph\n        the network\n\n    pct : float\n        A percentage (between 0 and 1)\n    \"\"\"\n    assert_is_percentage(pct)\n    edges = G.edges()\n    m = len(edges)\n    to_remove = int(m * pct)\n\n    log.debug(\"Will remove %d edges of %d (%f)\", to_remove, m, pct)\n    G.remove_edges_from(random.sample(list(edges), to_remove))\n\n\ndef add_remove_random_edges(G, pct_add, pct_remove):\n    \"\"\"Randomly add edges to and remove edges from G\n\n    Parameters\n    ----------\n    G : a networkx.Graph\n        the network\n\n    pct_add : float\n        A percentage (between 0 and 1)\n\n    pct_remove : float\n        A percentage (between 0 and 1)\n    \"\"\"\n    assert_is_percentage(pct_add)\n    assert_is_percentage(pct_remove)\n    edges = G.edges()\n    m = len(edges)\n    to_add = int(m * pct_add)\n    to_remove = int(m * pct_remove)\n    log.debug(\n        \"Will add %d (%f) edges to and remove %d (%f) edges of %d\",\n        to_add,\n        pct_add,\n        to_remove,\n        pct_remove,\n        m,\n    )\n\n    new_edges = set(nx.non_edges(G))\n    G.remove_edges_from(random.sample(list(edges), to_remove))\n    G.add_edges_from(random.sample(list(new_edges), to_add))\n"
  },
  {
    "path": "linkpred/network/algorithms.py",
    "content": "import logging\n\nimport networkx as nx\nimport numpy as np\n\nlog = logging.getLogger(__name__)\n\n__all__ = [\"rooted_pagerank\", \"simrank\"]\n\n\ndef rooted_pagerank(G, root, alpha=0.85, beta=0, weight=\"weight\"):\n    \"\"\"Return the rooted PageRank of all nodes with respect to node `root`\n\n    Parameters\n    ----------\n\n    G : a networkx.(Di)Graph\n        network to compute PR on\n\n    root : a node from the network\n        the node that will be the starting point of all random walks\n\n    alpha : float\n        PageRank probability that we will advance to a neighbour of the\n        current node in a random walk\n\n    beta : float or int\n        Normally, we return to the root node with probability 1 - alpha.\n        With this parameter, we can also advance to a random other node in the\n        network with probability beta. Thus, we get back to the root node with\n        probability 1 - alpha - beta. This is off (0) by default.\n\n    weight : string or None\n        The edge attribute that holds the numerical value used for\n        the edge weight.  If None then treat as unweighted.\n\n    \"\"\"\n    personalization = dict.fromkeys(G, beta)\n    personalization[root] = 1 - beta\n\n    return nx.pagerank(G, alpha, personalization, weight=weight)\n\n\ndef simrank(G, nodelist=None, c=0.8, num_iterations=10, weight=\"weight\"):\n    r\"\"\"Calculate SimRank matrix for nodes in nodelist\n\n    SimRank is defined as:\n\n    .. math ::\n\n        sim(u, v) = \\frac{c}{|N(u)| |N(v)|} \\sum_{p \\in N(u)}\n                    \\sum_{q \\in N(v)} sim(p, q)\n\n    Parameters\n    ----------\n    G : a networkx.Graph\n        network\n\n    nodelist : collection of nodes, optional\n        nodes to calculate SimRank for (default: all)\n\n    c : float, optional\n        decay factor, determines how quickly similarity decreases\n\n    num_iterations : int, optional\n        number of iterations to calculate\n\n    weight: string or None, optional\n        If None, all edge weights are considered equal.\n        Otherwise holds the name of the edge attribute used as weight.\n\n    \"\"\"\n    n = len(G)\n    M = raw_google_matrix(G, nodelist=nodelist, weight=weight)\n    sim = np.identity(n, dtype=np.float32)\n    for i in range(num_iterations):\n        log.debug(\"Starting SimRank iteration %d\", i)\n        temp = c * M.T @ sim @ M\n        sim = temp + np.identity(n) - np.diag(np.diag(temp))\n    return sim\n\n\ndef raw_google_matrix(G, nodelist=None, weight=\"weight\"):\n    \"\"\"Calculate the raw Google matrix (stochastic without teleportation)\"\"\"\n    n = len(G)\n    if n == 0:\n        msg = \"Empty network, cannot calculate Google matrix\"\n        raise ValueError(msg)\n    M = nx.to_numpy_array(G, nodelist=nodelist, dtype=np.float32, weight=weight)\n\n    # Find 'dangling' nodes, i.e. nodes whose row's sum = 0\n    dangling = np.where(M.sum(axis=1) == 0)\n    # add constant to dangling nodes' row\n    for d in dangling[0]:\n        M[d] = 1.0 / n\n    # Normalize. We now have the 'raw' Google matrix (cf. example on p. 11 of\n    # Langville & Meyer (2006)).\n    M = M / M.sum(axis=1)\n    return M\n"
  },
  {
    "path": "linkpred/predictors/__init__.py",
    "content": "from .base import *\nfrom .eigenvector import *\nfrom .misc import *\nfrom .neighbour import *\nfrom .path import *\n"
  },
  {
    "path": "linkpred/predictors/base.py",
    "content": "import contextlib\n\nfrom .util import neighbourhood\n\n__all__ = [\"Predictor\", \"all_predictors\"]\n\n\nclass Predictor:\n    \"\"\"\n    Predictor based on graph structure\n\n    This can also be used for bipartite networks or other networks\n    involving nodes that should not be included in the predictions.\n    To distinguish between 'eligible' and 'non-eligible' nodes, the\n    graph can set a node attribute that returns true for eligible\n    nodes and false for non-eligible ones.\n\n    For instance:\n\n    >>> import networkx as nx\n    >>> B = nx.Graph()\n    >>> # Add the node attribute \"bipartite\"\n    >>> B.add_nodes_from([1,2,3,4], bipartite=0)\n    >>> B.add_nodes_from(['a','b','c'], bipartite=1)\n    >>> B.add_edges_from([(1,'a'), (1,'b'), (2,'b'), (2,'c'),\n    ...                   (3,'c'), (4,'a')])\n    >>> p = Predictor(B, eligible='bipartite')\n    >>> p.eligible_node(1)\n    0\n    >>> sorted(p.eligible_nodes())\n    ['a', 'b', 'c']\n\n    \"\"\"\n\n    def __init__(self, G, eligible=None, excluded=None):\n        \"\"\"\n        Initialize predictor\n\n        Arguments\n        ---------\n        G : nx.Graph\n            a graph\n\n        eligible : a string or None\n            If this is a string, it is used to distinguish between eligible\n            and non-eligible nodes. We only try to predict links between\n            two eligible nodes.\n\n        excluded : iterable or None\n            A list or iterable of node pairs that should be excluded (i.e., not\n            predicted). This is useful to, for instance, make sure that we only\n            predict new links that are not currently in G.\n\n        \"\"\"\n        self.G = G\n        self.eligible_attr = eligible\n        self.name = self.__class__.__name__\n        self.excluded = [] if excluded is None else excluded\n\n        # Add a decorator to predict(), to do the necessary postprocessing for\n        # filtering out links if `excluded` is not empty. We do this in\n        # __init__() such that child classes need not be changed.\n        def add_postprocessing(func):\n            def predict_and_postprocess(*args, **kwargs):\n                scoresheet = func(*args, **kwargs)\n                for u, v in self.excluded:\n                    with contextlib.suppress(KeyError):\n                        del scoresheet[(u, v)]\n                return scoresheet\n\n            predict_and_postprocess.__name__ = func.__name__\n            predict_and_postprocess.__doc__ = func.__doc__\n            predict_and_postprocess.__dict__.update(func.__dict__)\n            return predict_and_postprocess\n\n        self.predict = add_postprocessing(self.predict)\n\n    def __str__(self):\n        return self.name\n\n    def __call__(self, *args, **kwargs):\n        return self.predict(*args, **kwargs)\n\n    def predict(self, *args, **kwargs):\n        raise NotImplementedError\n\n    def eligible(self, u, v):\n        \"\"\"Check if link between nodes u and v is eligible\n\n        Eligibility allows us to ignore some nodes/links for link prediction.\n\n        \"\"\"\n        return self.eligible_node(u) and self.eligible_node(v) and u != v\n\n    def eligible_node(self, v):\n        \"\"\"Check if node v is eligible\n\n        Eligibility allows us to ignore some nodes/links for link prediction.\n\n        \"\"\"\n        if self.eligible_attr is None:\n            return True\n        return self.G.nodes[v][self.eligible_attr]\n\n    def eligible_nodes(self):\n        \"\"\"Get list of eligible nodes\n\n        Eligibility allows us to ignore some nodes/links for link prediction.\n\n        \"\"\"\n        return [v for v in self.G if self.eligible_node(v)]\n\n    def likely_pairs(self, k=2):\n        \"\"\"\n        Yield node pairs from the same neighbourhood\n\n        Arguments\n        ---------\n        k : int\n            size of the neighbourhood (e.g., if k = 2, the neighbourhood\n            consists of all nodes that are two links away)\n\n        \"\"\"\n        for a in self.G.nodes():\n            if not self.eligible_node(a):\n                continue\n            for b in neighbourhood(self.G, a, k):\n                if not self.eligible_node(b):\n                    continue\n                yield (a, b)\n\n\ndef all_predictors():\n    \"\"\"Returns a list of all predictors\"\"\"\n    from operator import itemgetter\n\n    from ..util import itersubclasses\n\n    predictors = sorted(\n        ((s, s.__name__) for s in itersubclasses(Predictor)), key=itemgetter(1)\n    )\n    return list(zip(*predictors))[0]\n"
  },
  {
    "path": "linkpred/predictors/eigenvector.py",
    "content": "import networkx as nx\n\nfrom ..evaluation import Scoresheet\nfrom ..network import rooted_pagerank, simrank\nfrom ..util import progressbar\nfrom .base import Predictor\n\n\nclass RootedPageRank(Predictor):\n    def predict(self, nbunch=None, alpha=0.85, beta=0, weight=\"weight\", k=None):\n        \"\"\"Predict using rooted PageRank.\n\n        Parameters\n        ----------\n\n        nbunch : iterable collection of nodes, optional\n            node(s) to calculate PR for (default: all)\n\n        alpha : float, optional\n            PageRank probability that we will advance to a neighbour of the\n            current node in a random walk\n\n        beta : float, optional\n            Normally, we return to the root node with probability 1 - alpha.\n            With this parameter, we can also advance to a random other node in\n            the network with probability beta. Thus, we get back to the root\n            node with probability 1 - alpha - beta. This is off (0) by default.\n\n        weight : string or None, optional\n            The edge attribute that holds the numerical value used for\n            the edge weight.  If None then treat as unweighted.\n\n        k : int or None, optional\n            If `k` is `None`, this predictor is applied to the entire network.\n            If `k` is an int, the predictor is applied to a subgraph consisting\n            of the k-neighbourhood of the current node.\n            Results are often very similar but much faster.\n\n        See documentation for linkpred.network.rooted_pagerank for these\n        parameters.\n\n        \"\"\"\n        res = Scoresheet()\n        if nbunch is None:\n            nbunch = self.G.nodes()\n        for u in progressbar(nbunch):\n            if not self.eligible_node(u):\n                continue\n            # Restrict to the k-neighbourhood subgraph if k is defined\n            G = self.G if k is None else nx.ego_graph(self.G, u, radius=k)\n\n            pagerank_scores = rooted_pagerank(G, u, alpha, beta, weight)\n            for v, w in pagerank_scores.items():\n                if w > 0 and u != v and self.eligible_node(v):\n                    res[(u, v)] += w\n        return res\n\n\nclass SimRank(Predictor):\n    def predict(self, c=0.8, num_iterations=10, weight=\"weight\"):\n        r\"\"\"Predict using SimRank\n\n        .. math ::\n            sim(u, v) = \\frac{c}{|N(u)| \\cdot |N(v)|} \\sum_{p \\in N(u)}\n                        \\sum_{q \\in N(v)} sim(p, q)\n\n        where `N(v)` is the set of neighbours of node `v`.\n\n        Parameters\n        ----------\n        c : float, optional\n            decay factor, determines how quickly similarity decreases\n\n        num_iterations : int, optional\n            number of iterations to calculate\n\n        weight: string or None, optional\n            If None, all edge weights are considered equal.\n            Otherwise holds the name of the edge attribute used as weight.\n\n        \"\"\"\n        res = Scoresheet()\n        nodelist = list(self.G.nodes)\n        sim = simrank(self.G, nodelist, c, num_iterations, weight)\n        (m, n) = sim.shape\n        assert m == n\n\n        for i in range(m):\n            # sim(a, b) = sim(b, a), leading to a 'mirrored' matrix.\n            # We start the column range at i + 1, such that we only look at the\n            # upper triangle in the matrix, excluding the diagonal:\n            # sim(a, a) = 1.\n            u = nodelist[i]\n            for j in range(i + 1, n):\n                if sim[i, j] > 0:\n                    v = nodelist[j]\n                    if self.eligible(u, v):\n                        res[(u, v)] = sim[i, j]\n        return res\n"
  },
  {
    "path": "linkpred/predictors/misc.py",
    "content": "import random\nfrom collections import defaultdict\n\nfrom ..evaluation import Scoresheet\nfrom ..util import all_pairs\nfrom .base import Predictor\n\n__all__ = [\"Community\", \"Copy\", \"Random\"]\n\n\nclass Community(Predictor):\n    def predict(self):  # pylint:disable=E0202\n        \"\"\"Predict using community structure\n\n        If two nodes belong to the same community, they are predicted to form\n        a link. This uses the Louvain algorithm, which determines communities\n        at different granularity levels: the finer grained the community, the\n        higher the resulting score.\n\n        This needs the python-louvain package. Install linkpred as follows:\n\n        $ pip install linkpred[community]\n\n        \"\"\"\n        try:\n            import community\n        except ImportError as err:\n            msg = (\n                \"Module 'community' could not be found. Please install linkpred with: \"\n                \"$ pip install linkpred[community]\"\n            )\n            raise ImportError(msg) from err\n\n        res = Scoresheet()\n        dendogram = community.generate_dendrogram(self.G)\n\n        for i in range(len(dendogram)):\n            partition = community.partition_at_level(dendogram, i)\n            communities = defaultdict(list)\n            weight = len(dendogram) - i  # Lower i, smaller communities\n\n            for n, com in partition.items():\n                communities[com].append(n)\n            for nodes in communities.values():\n                for u, v in all_pairs(nodes):\n                    if not self.eligible(u, v):\n                        continue\n                    res[(u, v)] += weight\n        return res\n\n\nclass Copy(Predictor):\n    def predict(self, weight=None):  # pylint:disable=E0202\n        \"\"\"Predict by copying the training network\n\n        If weights are used, the likelihood score is equal to the link weight.\n\n        This predictor is mostly intended as a sort of baseline. By definition,\n        it only yields predictions if we do not exclude links from the training\n        network (with `excluded`).\n\n        Parameters\n        ----------\n        weight : None or string, optional\n            If None, all edge weights are considered equal.\n            Otherwise holds the name of the edge attribute used as weight.\n\n        \"\"\"\n        if weight is None:\n            return Scoresheet.fromkeys(self.G.edges(), 1)\n        return Scoresheet(((u, v), d[weight]) for u, v, d in self.G.edges(data=True))\n\n\nclass Random(Predictor):\n    def predict(self):  # pylint:disable=E0202\n        \"\"\"Predict randomly\n\n        This predictor can be used as a baseline.\n\n        \"\"\"\n        res = Scoresheet()\n        for a, b in all_pairs(self.eligible_nodes()):\n            res[(a, b)] = random.random()\n        return res\n"
  },
  {
    "path": "linkpred/predictors/neighbour.py",
    "content": "import math\n\nfrom ..evaluation import Scoresheet\nfrom ..util import all_pairs\nfrom .base import Predictor\nfrom .util import (\n    neighbourhood,\n    neighbourhood_intersection_size,\n    neighbourhood_size,\n    neighbourhood_union_size,\n)\n\n__all__ = [\n    \"AdamicAdar\",\n    \"AssociationStrength\",\n    \"CommonNeighbours\",\n    \"Cosine\",\n    \"DegreeProduct\",\n    \"Jaccard\",\n    \"MaxOverlap\",\n    \"MinOverlap\",\n    \"NMeasure\",\n    \"Pearson\",\n    \"ResourceAllocation\",\n]\n\n\nclass AdamicAdar(Predictor):\n    def predict(self, weight=None):\n        \"\"\"Predict by Adamic/Adar measure of neighbours\n\n        Parameters\n        ----------\n        weight : None or string, optional\n            If None, all edge weights are considered equal.\n            Otherwise holds the name of the edge attribute used as weight.\n\n        \"\"\"\n        res = Scoresheet()\n        for a, b in self.likely_pairs():\n            intersection = set(neighbourhood(self.G, a)) & set(neighbourhood(self.G, b))\n            w = 0\n            for c in intersection:\n                if weight is not None:\n                    numerator = self.G[a][c][weight] * self.G[b][c][weight]\n                else:\n                    numerator = 1\n                w += numerator / math.log(neighbourhood_size(self.G, c, weight))\n            if w > 0:\n                res[(a, b)] = w\n        return res\n\n\nclass AssociationStrength(Predictor):\n    def predict(self, weight=None):\n        \"\"\"Predict by association strength of neighbours\n\n        Parameters\n        ----------\n        weight : None or string, optional\n            If None, all edge weights are considered equal.\n            Otherwise holds the name of the edge attribute used as weight.\n\n        \"\"\"\n        res = Scoresheet()\n        for a, b in self.likely_pairs():\n            w = neighbourhood_intersection_size(self.G, a, b, weight) / (\n                neighbourhood_size(self.G, a, weight)\n                * neighbourhood_size(self.G, b, weight)\n            )\n            if w > 0:\n                res[(a, b)] = w\n        return res\n\n\nclass CommonNeighbours(Predictor):\n    def predict(self, alpha=1.0, weight=None):\n        r\"\"\"Predict using common neighbours\n\n        This is loosely based on Opsahl et al. (2010):\n\n        .. math ::\n\n            k(u, v) = |N(u) \\cap N(v)|\n            s(u, v) = \\sum_{i=1}^n x_i \\cdot y_i\n            W(u, v) = k(u, v)^{1 - \\alpha} \\cdot s(u, v)^{\\alpha}\n\n        Parameters\n        ----------\n        alpha : float, optional\n            If alpha = 0, weights are ignored. If alpha = 1, only weights are\n            used (ignoring the number of intermediary nodes).\n\n        weight : None or string, optional\n            If None, all edge weights are considered equal.\n            Otherwise holds the name of the edge attribute used as weight.\n\n        \"\"\"\n        res = Scoresheet()\n        for a, b in self.likely_pairs():\n            if weight is None or alpha == 0.0:\n                w = neighbourhood_intersection_size(self.G, a, b, weight=None)\n            elif alpha == 1.0:\n                w = neighbourhood_intersection_size(self.G, a, b, weight=weight)\n            else:\n                k = neighbourhood_intersection_size(self.G, a, b, weight=None)\n                s = neighbourhood_intersection_size(self.G, a, b, weight=weight)\n                w = (k ** (1.0 - alpha)) * (s**alpha)\n            if w > 0:\n                res[(a, b)] = w\n        return res\n\n\nclass Cosine(Predictor):\n    def predict(self, weight=None):\n        \"\"\"Predict by cosine measure of neighbours\n\n        Parameters\n        ----------\n        weight : None or string, optional\n            If None, all edge weights are considered equal.\n            Otherwise holds the name of the edge attribute used as weight.\n\n        \"\"\"\n        res = Scoresheet()\n        for a, b in self.likely_pairs():\n            w = neighbourhood_intersection_size(self.G, a, b, weight) / math.sqrt(\n                neighbourhood_size(self.G, a, weight)\n                * neighbourhood_size(self.G, b, weight)\n            )\n            if w > 0:\n                res[(a, b)] = w\n        return res\n\n\nclass DegreeProduct(Predictor):\n    def predict(self, weight=None, minimum=1):\n        \"\"\"Predict by degree product (preferential attachment)\n\n        Parameters\n        ----------\n        weight : None or string, optional\n            If None, all edge weights are considered equal.\n            Otherwise holds the name of the edge attribute used as weight.\n\n        minimum : int, optional (default = 1)\n            If the degree product is below this minimum, the corresponding\n            prediction is ignored.\n\n        \"\"\"\n        res = Scoresheet()\n        for a, b in all_pairs(self.eligible_nodes()):\n            w = neighbourhood_size(self.G, a, weight) * neighbourhood_size(\n                self.G, b, weight\n            )\n            if w >= minimum:\n                res[(a, b)] = w\n        return res\n\n\nclass Jaccard(Predictor):\n    def predict(self, weight=None):\n        \"\"\"Predict by Jaccard index of neighbours\n\n        Parameters\n        ----------\n        weight : None or string, optional\n            If None, all edge weights are considered equal.\n            Otherwise holds the name of the edge attribute used as weight.\n\n        \"\"\"\n        res = Scoresheet()\n        for a, b in self.likely_pairs():\n            # Best performance: weighted numerator, unweighted denominator.\n            numerator = neighbourhood_intersection_size(self.G, a, b, weight)\n            denominator = neighbourhood_union_size(self.G, a, b, weight)\n            w = numerator / denominator\n            if w > 0:\n                res[(a, b)] = w\n        return res\n\n\nclass NMeasure(Predictor):\n    def predict(self, weight=None):\n        \"\"\"Predict by N measure of neighbours\n\n        The N measure was defined by Egghe (2009).\n\n        Parameters\n        ----------\n        weight : None or string, optional\n            If None, all edge weights are considered equal.\n            Otherwise holds the name of the edge attribute used as weight.\n\n        \"\"\"\n        res = Scoresheet()\n        for a, b in self.likely_pairs():\n            w = (\n                math.sqrt(2)\n                * neighbourhood_intersection_size(self.G, a, b, weight)\n                / math.sqrt(\n                    neighbourhood_size(self.G, a, weight) ** 2\n                    + neighbourhood_size(self.G, b, weight) ** 2\n                )\n            )\n            if w > 0:\n                res[(a, b)] = w\n        return res\n\n\ndef _predict_overlap(predictor, function, weight=None):\n    res = Scoresheet()\n    for a, b in predictor.likely_pairs():\n        # Best performance: weighted numerator, unweighted denominator.\n        numerator = neighbourhood_intersection_size(predictor.G, a, b, weight)\n        denominator = function(\n            neighbourhood_size(predictor.G, a, weight),\n            neighbourhood_size(predictor.G, b, weight),\n        )\n        w = numerator / denominator\n        if w > 0:\n            res[(a, b)] = w\n    return res\n\n\nclass MaxOverlap(Predictor):\n    def predict(self, weight=None):\n        \"\"\"Predict by maximum overlap between neighbours\n\n        Parameters\n        ----------\n        weight : None or string, optional\n            If None, all edge weights are considered equal.\n            Otherwise holds the name of the edge attribute used as weight.\n\n        \"\"\"\n        return _predict_overlap(self, max, weight)\n\n\nclass MinOverlap(Predictor):\n    def predict(self, weight=None):\n        \"\"\"Predict by minimum overlap between neighbours\n\n        Parameters\n        ----------\n        weight : None or string, optional\n            If None, all edge weights are considered equal.\n            Otherwise holds the name of the edge attribute used as weight.\n\n        \"\"\"\n        return _predict_overlap(self, min, weight)\n\n\nclass Pearson(Predictor):\n    def predict(self, weight=None):\n        \"\"\"Predict by Pearson correlation between neighbours\n\n        Parameters\n        ----------\n        weight : None or string, optional\n            If None, all edge weights are considered equal.\n            Otherwise holds the name of the edge attribute used as weight.\n\n        \"\"\"\n        res = Scoresheet()\n        # 'Full' Pearson looks at all possible pairs. Since those are likely\n        # of little value for link prediction, we restrict ourselves to pairs\n        # with at least one common neighbour.\n        for a, b in self.likely_pairs():\n            n = len(self.G)\n            a_l2norm = neighbourhood_size(self.G, a, weight)\n            b_l2norm = neighbourhood_size(self.G, b, weight)\n            a_l1norm = neighbourhood_size(self.G, a, weight, power=1)\n            b_l1norm = neighbourhood_size(self.G, b, weight, power=1)\n            intersect = neighbourhood_intersection_size(self.G, a, b, weight)\n\n            numerator = (n * intersect) - (a_l1norm * b_l1norm)\n            denominator = math.sqrt(n * a_l2norm - a_l1norm**2) * math.sqrt(\n                n * b_l2norm - b_l1norm**2\n            )\n\n            w = numerator / denominator\n            if w > 0:\n                res[(a, b)] = w\n        return res\n\n\nclass ResourceAllocation(Predictor):\n    def predict(self, weight=None):\n        \"\"\"Predict with resource allocation index of neighbours\n\n        Resource allocation was defined by Zhou, Lu & Zhang (2009, Eur. Phys.\n        J. B, 71, 623).\n\n        Parameters\n        ----------\n        weight : None or string, optional\n            If None, all edge weights are considered equal.\n            Otherwise holds the name of the edge attribute used as weight.\n\n        \"\"\"\n        res = Scoresheet()\n        for a, b in self.likely_pairs():\n            intersection = set(neighbourhood(self.G, a)) & set(neighbourhood(self.G, b))\n            w = 0\n            for c in intersection:\n                if weight is not None:\n                    numerator = float(self.G[a][c][weight] * self.G[b][c][weight])\n                else:\n                    numerator = 1\n                w += numerator / neighbourhood_size(self.G, c, weight)\n            if w > 0:\n                res[(a, b)] = w\n        return res\n"
  },
  {
    "path": "linkpred/predictors/path.py",
    "content": "import networkx as nx\n\nfrom ..evaluation import Scoresheet\nfrom ..util import progressbar\nfrom .base import Predictor\n\n__all__ = [\"GraphDistance\", \"Katz\"]\n\n\nclass GraphDistance(Predictor):\n    def predict(self, weight=\"weight\", alpha=1):\n        r\"\"\"Predict by graph distance\n\n        This is based on the dissimilarity measures of Egghe & Rousseau (2003):\n\n        $d(i, j) = \\min(\\sum 1/w_k)$\n\n        The parameter alpha was introduced by Opsahl et al. (2010):\n\n        $d_\\alpha(i, j) = \\min(\\sum 1 / w_k^\\alpha)$\n\n        If alpha = 0 or weight is None, we determine unweighted graph distance,\n        i.e. only keep track of number of intermediate nodes and not of edge\n        weights. If alpha = 1, we only keep track of edge weights and not of\n        the number of intermediate nodes. (In practice, setting alpha equal to\n        around 0.1 seems to yield the best results.)\n\n        Parameters\n        ----------\n        weight : None or string, optional\n            If None, all edge weights are considered equal.\n            Otherwise holds the name of the edge attribute used as weight.\n\n        alpha : float\n            Parameter to determine relative importance of intermediate\n            link strength\n\n        \"\"\"\n        res = Scoresheet()\n\n        if weight is None:\n            G = self.G\n        else:\n            # We assume that edge weights denote proximities\n            G = nx.Graph()\n            G.add_weighted_edges_from(\n                (u, v, 1 / d[weight] ** alpha) for u, v, d in self.G.edges(data=True)\n            )\n\n        dist = nx.shortest_path_length(G, weight=weight)\n        for a, others in dist:\n            if not self.eligible_node(a):\n                continue\n            for b, length in others.items():\n                if a == b or not self.eligible_node(b):\n                    continue\n                w = 1 / length\n                res[(a, b)] = w\n        return res\n\n\ndef matrix_power(arr, n):\n    \"\"\"Matrix power: perform matrix multiplication n times\"\"\"\n    if n < 1:\n        msg = \"Expected power equal to 1 or higher\"\n        raise ValueError(msg)\n    if n == 1:\n        return arr\n\n    return arr @ matrix_power(arr, n - 1)\n\n\nclass Katz(Predictor):\n    def predict(self, beta=0.001, max_power=5, weight=\"weight\", dtype=None):\n        \"\"\"Predict by Katz (1953) measure\n\n        Let `A` be an adjacency matrix for the directed network `G`.\n        Then, each element `a_{ij}` of `A^k` (the `k`-th power of `A`) has a\n        value equal to the number of walks with length `k` from `i` to `j`.\n\n        The probability of a link rapidly decreases as the walks grow longer.\n        Katz therefore introduces an extra parameter (here beta) to weigh\n        longer walks less.\n\n        Parameters\n        ----------\n        beta : a float\n            the value of beta in the formula of the Katz equation\n\n        max_power : an int\n            the maximum number of powers to take into account\n\n        weight : string or None\n            The edge attribute that holds the numerical value used for\n            the edge weight.  If None then treat as unweighted.\n\n        dtype : a data type\n            data type of edge weights\n\n        \"\"\"\n        nodelist = list(self.G.nodes)\n        adj = nx.to_scipy_sparse_array(self.G, dtype=dtype, weight=weight)\n        res = Scoresheet()\n\n        for k in progressbar(range(1, max_power + 1), \"Computing matrix powers: \"):\n            # The below method is found to be fastest for iterating through a\n            # sparse matrix, see\n            # http://stackoverflow.com/questions/4319014/\n            matrix = matrix_power(adj, k).tocoo()\n            for i, j, d in zip(matrix.row, matrix.col, matrix.data):\n                if i == j:\n                    continue\n                u, v = nodelist[i], nodelist[j]\n                if self.eligible(u, v):\n                    w = d * (beta**k)\n                    res[(u, v)] += w\n\n        # We count double in case of undirected networks ((i, j) and (j, i))\n        if not self.G.is_directed():\n            for pair in res:\n                res[pair] /= 2\n\n        return res\n"
  },
  {
    "path": "linkpred/predictors/util.py",
    "content": "import networkx as nx\n\n\ndef neighbourhood(G, n, k=1):\n    \"\"\"Get k-neighbourhood of node n\"\"\"\n    if k == 1:\n        return G[n]\n    dist = nx.single_source_shortest_path_length(G, n, k)\n    del dist[n]\n    return dist.keys()\n\n\ndef neighbourhood_intersection_size(G, a, b, weight=None, k=1):\n    \"\"\"Get the summed weight of the common neighbours of a and b\n\n    If weighted, we use the sum of the weight products. This is equivalent\n    to the vector-based interpretation (dot product of the two vectors).\n\n    \"\"\"\n    common_neighbours = set(neighbourhood(G, a, k)) & set(neighbourhood(G, b, k))\n    if weight:\n        return sum(G[a][n][weight] * G[b][n][weight] for n in common_neighbours)\n\n    return len(common_neighbours)\n\n\ndef neighbourhood_size(G, u, weight=None, k=1, power=2):\n    \"\"\"Get the weight of the neighbours of u\n\n    If weighted, we use the sum of the squared edge weight for compatibility\n    with the vector-based measures.\n\n    \"\"\"\n    # The fast route for default options\n    if weight is None and k == 1:\n        return len(G[u])\n    # The slow route for everything else\n    neighbours = neighbourhood(G, u, k)\n    return (\n        sum(G[u][v][weight] ** power for v in neighbours) if weight else len(neighbours)\n    )\n\n\ndef neighbourhood_union_size(G, a, b, weight=None, k=1, power=2):\n    \"\"\"Get the weight of the neighbours union of a and b\"\"\"\n    a_neighbours = set(neighbourhood(G, a, k))\n    b_neighbours = set(neighbourhood(G, b, k))\n    if weight:\n        return (\n            sum(G[a][n][weight] ** power for n in a_neighbours)\n            + sum(G[b][n][weight] ** power for n in b_neighbours)\n            - sum(\n                G[a][n][weight] * G[b][n][weight] for n in a_neighbours & b_neighbours\n            )\n        )\n\n    return len(a_neighbours | b_neighbours)\n"
  },
  {
    "path": "linkpred/preprocess.py",
    "content": "import logging\n\nimport networkx as nx\n\nlog = logging.getLogger(__name__)\n\n\ndef without_low_degree_nodes(G, minimum=1, eligible=None):\n    \"\"\"Return a copy of the graph without nodes with degree below minimum\n\n    arguments\n    ---------\n    g : a networkx.graph\n\n    minimum : int\n        minimum node degree\n\n    eligible : none or string\n        only eligible nodes are considered for removal\n\n    \"\"\"\n\n    def low_degree(G, threshold):\n        \"\"\"Get eligible nodes whose degree is below the threshold\"\"\"\n        if eligible is None:\n            return [n for n, d in G.degree() if d < threshold]\n\n        return [n for n, d in G.degree() if d < threshold and G.nodes[n][eligible]]\n\n    to_remove = low_degree(G, minimum)\n    H = G.copy()\n    H.remove_nodes_from(to_remove)\n    log.info(\"Removed %d nodes (degree < %d)\", len(to_remove), minimum)\n\n    return H\n\n\ndef without_uncommon_nodes(networks, eligible=None):\n    \"\"\"Return list of networks without nodes not common to all\n\n    Arguments\n    ---------\n    networks : an iterable of `networkx.Graph`s\n\n    eligible : None or string\n        only eligible nodes are considered for removal\n\n    Example\n    -------\n    >>> import networkx as nx\n    >>> A, B = nx.Graph(), nx.Graph()\n    >>> A.add_nodes_from('abcd')\n    >>> B.add_nodes_from('cdef')\n    >>> A2, B2 = without_uncommon_nodes((A, B))\n    >>> sorted(A2.nodes())\n    ['c', 'd']\n    >>> sorted(B2.nodes())\n    ['c', 'd']\n\n    \"\"\"\n\n    def items_outside(G, nbunch):\n        \"\"\"Get eligible nodes outside nbunch\"\"\"\n        if eligible is None:\n            return [n for n in G.nodes() if n not in nbunch]\n\n        return [n for n in G.nodes() if G.nodes[n][eligible] and n not in nbunch]\n\n    common = set.intersection(*[set(G) for G in networks])\n    new_networks = []\n    for G in networks:\n        to_remove = items_outside(G, common)\n        H = G.copy()\n        H.remove_nodes_from(to_remove)\n        new_networks.append(H)\n        log.info(\"Removed %d nodes (not common)\", len(to_remove))\n\n    return new_networks\n\n\ndef without_selfloops(G):\n    \"\"\"return copy of G without selfloop edges\"\"\"\n    H = G.copy()\n    num_loops = nx.number_of_selfloops(G)\n\n    if num_loops:\n        log.warning(\"Network contains %d self-loops. Removing...\", num_loops)\n        H.remove_edges_from(nx.selfloop_edges(G))\n\n    return H\n"
  },
  {
    "path": "linkpred/util.py",
    "content": "import itertools\nimport sys\n\n\ndef all_pairs(iterable):\n    \"\"\"Return iterator over all possible pairs in l\"\"\"\n    return itertools.combinations(iterable, 2)\n\n\ndef progressbar(it, prefix=\"\", size=60):\n    \"\"\"Show progress bar\n\n    http://code.activestate.com/recipes/576986-progress-bar-for-console-programs-as-iterator/\n\n    \"\"\"\n    count = len(it)\n\n    def _show(_i):\n        x = int(size * _i / count)\n        sys.stdout.write(\n            \"%s[%s%s] %i/%i\\r\" % (prefix, \"#\" * x, \".\" * (size - x), _i, count)\n        )\n        sys.stdout.flush()\n\n    _show(0)\n    for i, item in enumerate(it, start=1):\n        yield item\n        _show(i)\n    sys.stdout.write(\"\\n\")\n    sys.stdout.flush()\n\n\ndef load_function(full_functionname):\n    \"\"\"Return the function given by full_functionname\n\n    This loads function names of the form 'module.submodule.function'\n\n    \"\"\"\n    try:\n        modulename, functionname = full_functionname.rsplit(\".\", 1)\n    except ValueError:\n        msg = f\"No module name given in {full_functionname}\"\n        raise ValueError(msg) from None\n\n    # Dynamically load module and function\n    __import__(modulename)\n    module = sys.modules[modulename]\n    return getattr(module, functionname)\n\n\ndef interpolate(curve):\n    \"\"\"Make curve decrease.\"\"\"\n    for i in range(-1, -len(curve), -1):\n        if curve[i] > curve[i - 1]:\n            curve[i - 1] = curve[i]\n    return curve\n\n\ndef itersubclasses(cls, _seen=None):\n    \"\"\"Generator over all subclasses of a given class, in depth first order.\n\n    Based on:\n    http://code.activestate.com/recipes/576949-find-all-subclasses-of-a-given-class/\n\n    \"\"\"\n    if _seen is None:\n        _seen = set()\n    try:\n        subs = cls.__subclasses__()\n    except TypeError:  # fails only when cls is type\n        subs = cls.__subclasses__(cls)\n    for sub in subs:\n        if sub not in _seen:\n            _seen.add(sub)\n            yield sub\n            for sub2 in itersubclasses(sub, _seen):\n                yield sub2\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"flit_core >=3.2,<4\"]\nbuild-backend = \"flit_core.buildapi\"\n\n[project]\nname = \"linkpred\"\nauthors = [{name = \"Raf Guns\", email = \"raf.guns@uantwerpen.be\"}]\nreadme = \"README.rst\"\ndynamic = [\"version\", \"description\"]\nclassifiers = [\n        \"Intended Audience :: Science/Research\",\n        \"Intended Audience :: Developers\",\n        \"License :: OSI Approved\",\n        \"Programming Language :: Python :: 3.8\",\n        \"Programming Language :: Python :: 3.9\",\n        \"Programming Language :: Python :: 3.10\",\n        \"Programming Language :: Python :: 3.11\",\n        \"Programming Language :: Python :: 3.12\",\n        \"Topic :: Software Development\",\n        \"Topic :: Scientific/Engineering\",\n        \"Development Status :: 4 - Beta\",\n        \"Natural Language :: English\",\n        \"Operating System :: OS Independent\",\n]\nrequires-python=\">=3.8\"\ndependencies = [\n        \"matplotlib>=3.5\",\n        \"networkx>=3.0\",\n        \"numpy>=1.23\",\n        \"pyyaml>=3.0\",\n        \"scipy>=1.10\",\n        \"smokesignal>=0.7\",\n]\n\n[project.scripts]\nlinkpred = \"linkpred.cli:main\"\n\n[project.optional-dependencies]\ndev = [\"pytest >=7.1\", \"pytest-cov\", \"tox>=4.4\"]\ncommunity = [\"python-louvain\"]\nall = [\"pytest >=7.1\", \"pytest-cov\", \"tox>=4.4\", \"python-louvain\"]\n\n[project.urls]\nHome = \"https://github.com/rafguns/linkpred/\"\n\n[tool.flit.module]\nname = \"linkpred\"\n\n[tool.ruff]\ntarget-version = \"py38\"\n# See https://beta.ruff.rs/docs/rules/\nselect = [\n    \"A\", # builtin shadowing\n    \"ARG\", # unsued arguments\n    \"B\", # bugbear\n    \"C4\", # comprehensions\n    \"C90\", # mccabe complexity\n    \"E\", # style errors\n    \"EM\", # error messages\n    \"F\", # flakes\n    \"FBT\", # boolean trap\n    \"G\", # logging format\n    \"I\", # import sorting\n    \"ISC\", # string concatenation\n    \"N\", # naming\n    \"PGH\", # pygrep-hooks\n    \"PIE\", # miscellaneous\n    \"PL\", # pylint\n    \"PT\", # pytest style\n    \"Q\", # quotes\n    \"RET\", # return\n    \"RSE\", # raise\n    \"RUF\", # Ruff\n    \"SIM\", # simplify\n    \"T20\", # print\n    \"UP\", # upgrade\n    \"W\", # style warnings\n    \"YTT\", # sys.version\n]\n\nignore = [\n    \"N803\", # Argument name `G` should be lowercase\n    \"N806\", # \"Variable `G` in function should be lowercase\"\n    \"PLR0913\", # Too many arguments to function call\n]\n\n[tool.ruff.per-file-ignores]\n# Ignore unused imports in __init__.py\n\"__init__.py\" = [\"F401\", \"F403\"]\n"
  },
  {
    "path": "pytest.ini",
    "content": "[pytest]\nfilterwarnings=\n    # will be fixed in networkx 2.5\n    ignore:.*is deprecated and will be removed in SciPy 2.0.0.*::networkx\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/test_addremove.py",
    "content": "import networkx as nx\nimport pytest\n\nfrom linkpred.network.addremove import (\n    add_random_edges,\n    add_remove_random_edges,\n    remove_random_edges,\n)\n\n\ndef test_add_random_edges():\n    G = nx.star_graph(10)\n    edges = list(G.edges())\n\n    add_random_edges(G, 0)\n    assert edges == list(G.edges())\n\n    add_random_edges(G, 0.5)\n    assert G.size() == 15\n    assert set(edges) < set(G.edges())\n\n    with pytest.raises(ValueError):\n        add_random_edges(G, 1.2)\n\n\ndef test_remove_random_edges():\n    G = nx.star_graph(10)\n    edges = list(G.edges())\n\n    remove_random_edges(G, 0)\n    assert edges == list(G.edges())\n\n    remove_random_edges(G, 0.5)\n    assert G.size() == 5\n    assert set(G.edges()) < set(edges)\n\n    with pytest.raises(ValueError):\n        remove_random_edges(G, 10)\n\n\ndef test_add_remove_random_edges():\n    G = nx.star_graph(10)\n    edges = list(G.edges())\n\n    add_remove_random_edges(G, 0, 0)\n    assert edges == list(G.edges())\n\n    add_remove_random_edges(G, 0.3, 0.4)\n    assert G.size() == 9\n    assert len(set(edges) & set(G.edges())) == 6\n\n    with pytest.raises(ValueError):\n        add_remove_random_edges(G, 0, 1.2)\n    with pytest.raises(ValueError):\n        add_remove_random_edges(G, 1.2, 0)\n"
  },
  {
    "path": "tests/test_cli.py",
    "content": "import os\nimport tempfile\nfrom contextlib import contextmanager\n\nimport pytest\n\nfrom linkpred.cli import get_config, handle_arguments, load_profile\n\n\n@contextmanager\ndef temp_empty_file():\n    fh = tempfile.NamedTemporaryFile(\"r\", delete=False)\n    yield fh.name\n    assert fh.read() == \"\"\n    fh.close()\n\n\nclass TestProfileFile:\n    def setup_method(self):\n        self.yaml_fd, self.yaml_fname = tempfile.mkstemp(suffix=\".yaml\")\n        self.json_fd, self.json_fname = tempfile.mkstemp(suffix=\".json\")\n        self.expected = {\n            \"predictors\": [\n                {\"name\": \"CommonNeighbours\", \"displayname\": \"Common neighbours\"},\n                {\"name\": \"Cosine\"},\n            ],\n            \"interpolation\": True,\n        }\n\n    def teardown_method(self):\n        for fd, fname in (\n            (self.yaml_fd, self.yaml_fname),\n            (self.json_fd, self.json_fname),\n        ):\n            os.close(fd)\n            os.unlink(fname)\n\n    def test_load_profile_yaml(self):\n        with open(self.yaml_fname, \"w\") as fh:\n            fh.write(\n                \"\"\"predictors:\n- name: CommonNeighbours\n  displayname: Common neighbours\n- name: Cosine\ninterpolation: true\"\"\"\n            )\n        profile = load_profile(self.yaml_fname)\n        assert profile == self.expected\n\n    def test_load_profile_json(self):\n        with open(self.json_fname, \"w\") as fh:\n            fh.write(\n                \"\"\"{\"predictors\":\n                [{\"name\": \"CommonNeighbours\",\n                \"displayname\": \"Common neighbours\"},\n                {\"name\": \"Cosine\"}],\n                \"interpolation\": true}\"\"\"\n            )\n        profile = load_profile(self.json_fname)\n        assert profile == self.expected\n\n    def test_load_profile_error(self):\n        with open(self.json_fname, \"w\") as fh:\n            fh.write(\"foobar\")\n        with pytest.raises(Exception):\n            load_profile(self.json_fname)\n\n        with open(self.yaml_fname, \"w\") as fh:\n            fh.write(\"{foobar\")\n        with pytest.raises(Exception):\n            load_profile(self.yaml_fname)\n\n    def test_get_config(self):\n        with open(self.yaml_fname, \"w\") as fh:\n            fh.write(\n                \"\"\"predictors:\n- name: CommonNeighbours\n  displayname: Common neighbours\n- name: Cosine\ninterpolation: true\"\"\"\n            )\n\n        fh = tempfile.NamedTemporaryFile(\"r\", delete=False)\n        with temp_empty_file() as training:\n            config = get_config([training, \"-P\", self.yaml_fname])\n            for k, v in self.expected.items():\n                assert config[k] == v\n\n        with temp_empty_file() as training:\n            config = get_config([fh.name, \"-P\", self.yaml_fname, \"-p\", \"Katz\", \"-i\"])\n            # Profile gets priority\n            for k, v in self.expected.items():\n                assert config[k] == v\n        fh.close()\n\n\ndef test_no_training_file():\n    with pytest.raises(SystemExit):\n        handle_arguments([])\n\n\ndef test_nonexisting_predictor():\n    with pytest.raises(SystemExit):\n        handle_arguments([\"some-network\", \"-p\", \"Aargh\"])\n\n\ndef test_handle_arguments():\n    expected = {\n        \"debug\": False,\n        \"quiet\": False,\n        \"output\": [\"recall-precision\"],\n        \"chart_filetype\": \"pdf\",\n        \"interpolation\": True,\n        \"predictors\": [],\n        \"exclude\": \"old\",\n        \"profile\": None,\n        \"training-file\": \"training\",\n    }\n\n    args = handle_arguments([\"training\"])\n    for k, v in expected.items():\n        assert args[k] == v\n\n    args = handle_arguments([\"training\", \"test\"])\n    for k, v in expected.items():\n        assert args[k] == v\n    assert args[\"test-file\"] == \"test\"\n\n    argstr = \"-p CommonNeighbours Cosine -o fmax \" \"recall-precision -- training\"\n    args = handle_arguments(argstr.split())\n    expected_special = {\n        \"predictors\": [\"CommonNeighbours\", \"Cosine\"],\n        \"output\": [\"fmax\", \"recall-precision\"],\n    }\n    for k, v in expected_special.items():\n        assert args[k] == v\n\n    args = handle_arguments([\"training\", \"-i\"])\n    assert args[\"interpolation\"] is False\n\n    args = handle_arguments([\"training\", \"-a\"])\n    assert args[\"exclude\"] == \"\"\n\n    args = handle_arguments([\"training\", \"-P\", \"foo.json\"])\n    assert args[\"profile\"] == \"foo.json\"\n\n    args = handle_arguments([\"training\", \"-f\", \"eps\"])\n    assert args[\"chart_filetype\"] == \"eps\"\n"
  },
  {
    "path": "tests/test_evaluation_static.py",
    "content": "import numpy as np\nimport pytest\n\nfrom linkpred.evaluation import (\n    BaseScoresheet,\n    EvaluationSheet,\n    Scoresheet,\n    StaticEvaluation,\n    UndefinedError,\n)\n\nfrom .utils import assert_array_equal, temp_file\n\n\nclass TestStaticEvaluation:\n    def setup_method(self):\n        self.ret = range(5)\n        self.rel = [3, 4, 5, 6]\n        self.num_universe = 20\n        self.universe = range(self.num_universe)\n\n    def test_init(self):\n        e = StaticEvaluation(self.ret, self.rel, self.universe)\n        assert len(e.tp) == 2\n        assert len(e.fp) == 3\n        assert len(e.tn) == 13\n        assert len(e.fn) == 2\n\n        e_no_universe = StaticEvaluation(self.ret, self.rel)\n        assert len(e.tp) == len(e_no_universe.tp)\n        assert len(e.fp) == len(e_no_universe.fp)\n        assert len(e.fn) == len(e_no_universe.fn)\n        assert e_no_universe.tn is None\n\n        e_num_universe = StaticEvaluation(self.ret, self.rel, self.num_universe)\n        assert len(e_num_universe.tp) == 2\n        assert len(e_num_universe.fp) == 3\n        assert len(e_num_universe.fn) == 2\n        assert len(e_num_universe.tp) == e_num_universe.num_tp\n        assert len(e_num_universe.fp) == e_num_universe.num_fp\n        assert len(e_num_universe.fn) == e_num_universe.num_fn\n        assert e_num_universe.num_tn == 13\n\n    def test_update_retrieved(self):\n        e = StaticEvaluation(self.ret, self.rel, self.universe)\n        e.update_retrieved([6, 7])\n        assert len(e.tp) == 3\n        assert len(e.fp) == 4\n        assert len(e.tn) == 12\n        assert len(e.fn) == 1\n\n        with pytest.raises(ValueError):\n            e.update_retrieved([1])  # fp\n        with pytest.raises(ValueError):\n            e.update_retrieved([3])  # tp\n        with pytest.raises(ValueError):\n            e.update_retrieved([\"a\"])\n\n    def test_update_retrieved_num_universe(self):\n        e = StaticEvaluation(self.ret, self.rel, self.num_universe)\n        e.update_retrieved([6, 7])\n        assert len(e.tp) == 3\n        assert len(e.fp) == 4\n        assert len(e.fn) == 1\n        assert e.num_tp == 3\n        assert e.num_fp == 4\n        assert e.num_tn == 12\n        assert e.num_fn == 1\n\n        with pytest.raises(ValueError):\n            e.update_retrieved([1])  # fp\n        with pytest.raises(ValueError):\n            e.update_retrieved([3])  # tp\n\n    def test_update_retrieved_full(self):\n        e = StaticEvaluation(relevant=range(5), universe=20)\n        e.update_retrieved(range(10))\n        e.update_retrieved(range(10, 20))\n        assert e.num_tp == 5\n        assert e.num_fp == 15\n        assert e.num_fn == 0\n        assert e.num_tn == 0\n\n    def test_ret_no_universe_subset(self):\n        with pytest.raises(ValueError):\n            StaticEvaluation([1, 2, \"a\"], [2, 3], range(10))\n\n    def test_rel_no_universe_subset(self):\n        with pytest.raises(ValueError):\n            StaticEvaluation([1, 2], [2, 3, \"a\"], range(10))\n\n    def test_ret_larger_than_universe(self):\n        with pytest.raises(ValueError):\n            StaticEvaluation(range(11), [2, 3], 10)\n\n    def test_rel_larger_than_universe(self):\n        with pytest.raises(ValueError):\n            StaticEvaluation([1, 2], range(11), 10)\n\n\nclass TestEvaluationSheet:\n    def setup_method(self):\n        self.rel = [3, 4, 5, 6]\n        self.scores = BaseScoresheet(\n            {7: 0.9, 4: 0.8, 6: 0.7, 1: 0.6, 3: 0.5, 5: 0.2, 2: 0.1}\n        )\n        self.num_universe = 20\n        self.universe = range(self.num_universe)\n\n    def test_init(self):\n        sheet = EvaluationSheet(self.scores, relevant=self.rel)\n        expected = np.array(\n            [\n                [0, 1, 2, 2, 3, 4, 4],\n                [1, 1, 1, 2, 2, 2, 3],\n                [4, 3, 2, 2, 1, 0, 0],\n                [-1, -1, -1, -1, -1, -1, -1],\n            ]\n        ).T\n        assert_array_equal(sheet.data, expected)\n\n        sheet = EvaluationSheet(self.scores, relevant=self.rel, universe=self.universe)\n        expected = np.array(\n            [\n                [0, 1, 2, 2, 3, 4, 4],\n                [1, 1, 1, 2, 2, 2, 3],\n                [4, 3, 2, 2, 1, 0, 0],\n                [15, 15, 15, 14, 14, 14, 13],\n            ]\n        ).T\n        assert_array_equal(sheet.data, expected)\n\n        sheet = EvaluationSheet(\n            self.scores, relevant=self.rel, universe=self.num_universe\n        )\n        # Same expected applies as above\n        assert_array_equal(sheet.data, expected)\n\n        data = np.array([[1, 0, 0, 1], [1, 1, 0, 0]])\n        sheet = EvaluationSheet(data)\n        assert_array_equal(sheet.data, data)\n\n    def test_to_file_from_file(self):\n        data = np.array([[1, 0, 0, 1], [1, 1, 0, 0]])\n        sheet = EvaluationSheet(data)\n\n        with temp_file() as fname:\n            sheet.to_file(fname)\n            newsheet = EvaluationSheet.from_file(fname)\n            assert_array_equal(sheet.data, newsheet.data)\n\n    def test_measures(self):\n        sheet_num_universe = EvaluationSheet(\n            self.scores, relevant=self.rel, universe=self.num_universe\n        )\n        sheet_universe = EvaluationSheet(\n            self.scores, relevant=self.rel, universe=self.universe\n        )\n        sheet_no_universe = EvaluationSheet(self.scores, relevant=self.rel)\n\n        # Measures that don't require universe\n\n        for sheet in (sheet_num_universe, sheet_universe, sheet_no_universe):\n            assert_array_equal(\n                sheet.precision(), np.array([0, 0.5, 2 / 3, 0.5, 3 / 5, 2 / 3, 4 / 7])\n            )\n            assert_array_equal(\n                sheet.recall(), np.array([0, 0.25, 0.5, 0.5, 0.75, 1, 1])\n            )\n\n        # Measures that do require universe\n\n        for sheet in (sheet_num_universe, sheet_universe):\n            # XXX The following ones look wrong?!\n            expected = np.array([1 / 16, 1 / 16, 1 / 16, 1 / 8, 1 / 8, 1 / 8, 3 / 16])\n            assert_array_equal(sheet.fallout(), expected)\n            expected = np.array([4 / 19, 3 / 18, 2 / 17, 2 / 16, 1 / 15, 0, 0])\n            assert_array_equal(sheet.miss(), expected)\n            expected = np.array([0.75, 0.8, 17 / 20, 0.8, 17 / 20, 0.9, 17 / 20])\n            assert_array_equal(sheet.accuracy(), expected)\n            assert_array_equal(sheet.generality(), 0.2)\n\n        with pytest.raises(UndefinedError):\n            sheet_no_universe.fallout()\n        with pytest.raises(UndefinedError):\n            sheet_no_universe.miss()\n        with pytest.raises(UndefinedError):\n            sheet_no_universe.accuracy()\n        with pytest.raises(UndefinedError):\n            sheet_no_universe.generality()\n\n    def test_measures_with_empty_rel_and_ret(self):\n        sheet1 = EvaluationSheet(Scoresheet(), [], [])\n        sheet2 = EvaluationSheet(Scoresheet(), [], 10)\n        sheet3 = EvaluationSheet(Scoresheet(), [])\n\n        for sheet in (sheet1, sheet2, sheet3):\n            for method in [\n                \"precision\",\n                \"recall\",\n                \"f_score\",\n                \"fallout\",\n                \"miss\",\n                \"accuracy\",\n                \"generality\",\n            ]:\n                with pytest.raises(UndefinedError):\n                    getattr(sheet, method)()\n\n    def test_f_score(self):\n        sheet = EvaluationSheet(self.scores, relevant=self.rel)\n        expected = np.array([0, 2 / 6, 4 / 7, 4 / 8, 6 / 9, 8 / 10, 8 / 11])\n        assert_array_equal(sheet.f_score(), expected)\n        # $F_\\beta = \\frac{\\beta^2 + 1 |rel \\cap ret|}{\\beta^2 |rel| + |ret|}$\n        expected = np.array(\n            [\n                0,\n                1.25 * 1 / (0.25 * 4 + 2),\n                1.25 * 2 / (0.25 * 4 + 3),\n                1.25 * 2 / (0.25 * 4 + 4),\n                1.25 * 3 / (0.25 * 4 + 5),\n                1.25 * 4 / (0.25 * 4 + 6),\n                1.25 * 4 / (0.25 * 4 + 7),\n            ]\n        )\n        assert_array_equal(sheet.f_score(0.5), expected)\n        expected = np.array(\n            [\n                0,\n                5 * 1 / (4 * 4 + 2),\n                5 * 2 / (4 * 4 + 3),\n                5 * 2 / (4 * 4 + 4),\n                5 * 3 / (4 * 4 + 5),\n                5 * 4 / (4 * 4 + 6),\n                5 * 4 / (4 * 4 + 7),\n            ]\n        )\n        assert_array_equal(sheet.f_score(2), expected)\n"
  },
  {
    "path": "tests/test_functional.py",
    "content": "# Should be at start of file\nimport matplotlib\n\nmatplotlib.use(\"Agg\")\n\nimport os\n\nfrom linkpred.cli import main\n\n\ndef test_simple_run():\n    # TODO do this test in a temp directory and clean up afterwards\n    num_files = len(os.listdir(\"examples\"))\n    main(\n        \"examples/inf1990-2004.net examples/inf2005-2009.net\"\n        \" -p CommonNeighbours --quiet\".split()\n    )\n    assert len(os.listdir(\"examples\")) == num_files + 1\n"
  },
  {
    "path": "tests/test_linkpred.py",
    "content": "# Should be at start of file\nimport io\n\nimport matplotlib\nimport networkx as nx\nimport pytest\nimport smokesignal\n\nimport linkpred\nfrom linkpred.evaluation.listeners import (\n    CacheEvaluationListener,\n    FMaxListener,\n    FScorePlotter,\n    RecallPrecisionPlotter,\n    ROCPlotter,\n)\n\nfrom .utils import temp_file\n\nmatplotlib.use(\"Agg\")\n\n\ndef test_imports():\n    linkpred.LinkPred\n    linkpred.read_network\n    linkpred.network\n    linkpred.exceptions\n    linkpred.evaluation\n    linkpred.predictors\n\n\ndef test_for_comparison():\n    from linkpred.evaluation import Pair\n    from linkpred.linkpred import for_comparison\n\n    G = nx.path_graph(10)\n    expected = {(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9)}\n    assert for_comparison(G) == expected\n\n    to_delete = [Pair(2, 3), Pair(8, 9)]\n    expected = {Pair(t) for t in expected}\n    expected = expected.difference(to_delete)\n    assert for_comparison(G, exclude=to_delete) == expected\n\n\ndef test_pretty_print():\n    from linkpred.linkpred import pretty_print\n\n    name = \"foo\"\n    assert pretty_print(name) == \"foo\"\n    params = {\"bar\": 0.1, \"baz\": 5}\n\n    # 2 possibilities because of hash randomization\n    assert pretty_print(name, params) in [\n        \"foo (baz = 5, bar = 0.1)\",\n        \"foo (bar = 0.1, baz = 5)\",\n    ]\n\n\ndef test_read_unknown_network_type():\n    with temp_file(suffix=\".foo\") as fname:\n        with pytest.raises(linkpred.exceptions.LinkPredError):\n            linkpred.read_network(fname)\n\n\ndef test_read_network():\n    with temp_file(suffix=\".net\") as fname:\n        with open(fname, \"w\") as fh:\n            fh.write(\n                \"\"\"*vertices 2\n1 \"A\"\n2 \"B\"\n*arcs 2\n1 2\n2 1\"\"\"\n            )\n        expected = nx.DiGraph()\n        expected.add_edges_from([(\"A\", \"B\"), (\"B\", \"A\")])\n\n        G = linkpred.read_network(fname)\n        assert set(G.edges()) == set(expected.edges())\n\n        with open(fname) as fh:\n            G = linkpred.read_network(fname)\n            assert set(G.edges()) == set(expected.edges())\n\n\ndef test_read_pajek():\n    from linkpred.linkpred import _read_pajek\n\n    with temp_file(suffix=\".net\") as fname:\n        with open(fname, \"w\") as fh:\n            fh.write(\n                \"\"\"*vertices 2\n1 \"A\"\n2 \"B\"\n*arcs 2\n1 2\n1 2\"\"\"\n            )\n        expected = nx.DiGraph()\n        expected.add_edges_from([(\"A\", \"B\")])\n\n        G = _read_pajek(fname)\n        assert isinstance(G, nx.DiGraph)\n        assert sorted(G.edges()) == sorted(expected.edges())\n\n        with open(fname, \"w\") as fh:\n            fh.write(\n                \"\"\"*vertices 2\n1 \"A\"\n2 \"B\"\n*edges 2\n1 2\n1 2\"\"\"\n            )\n        expected = nx.Graph()\n        expected.add_edges_from([(\"A\", \"B\")])\n\n        G = _read_pajek(fname)\n        assert isinstance(G, nx.Graph)\n        assert sorted(G.edges()) == sorted(expected.edges())\n\n\ndef test_LinkPred_without_predictors():\n    with pytest.raises(linkpred.exceptions.LinkPredError):\n        linkpred.LinkPred()\n\n\nclass TestLinkpred:\n    def teardown_method(self):\n        smokesignal.clear_all()\n\n    def config_file(self, training=False, test=False, **kwargs):\n        config = {\"predictors\": [\"Random\"], \"label\": \"testing\"}\n\n        # add training and test files, if needed\n        for var, name, fname, data in (\n            (\n                training,\n                \"training\",\n                \"foo.net\",\n                b\"*Vertices 3\\n1 A\\n2 B\\n3 C\\n\" b\"*Edges 1\\n1 2 1\\n\",\n            ),\n            (\n                test,\n                \"test\",\n                \"bar.net\",\n                b\"*Vertices 3\\n1 A\\n2 B\\n3 C\\n\" b\"*Edges 1\\n3 2 1\\n\",\n            ),\n        ):\n            if var:\n                fh = io.BytesIO()\n                fh.name = fname\n                fh.write(data)\n                fh.seek(0)\n                config[f\"{name}-file\"] = fh\n\n        config.update(kwargs)\n        return config\n\n    def test_init(self):\n        lp = linkpred.LinkPred(self.config_file())\n        assert lp.config[\"label\"] == \"testing\"\n        assert lp.training is None\n\n        lp = linkpred.LinkPred(self.config_file(training=True))\n        assert isinstance(lp.training, nx.Graph)\n        assert len(lp.training.nodes()) == 3\n        assert len(lp.training.edges()) == 1\n        assert lp.test is None\n\n    def test_excluded(self):\n        for value, expected in zip(\n            (\"\", \"old\", \"new\"), (set(), {(\"A\", \"B\")}, {(\"B\", \"C\"), (\"A\", \"C\")})\n        ):\n            lp = linkpred.LinkPred(self.config_file(training=True, exclude=value))\n            assert {tuple(sorted(p)) for p in lp.excluded} == expected\n        with pytest.raises(linkpred.exceptions.LinkPredError):\n            lp = linkpred.LinkPred(self.config_file(exclude=\"bla\"))\n            lp.excluded\n\n    def test_preprocess_only_training(self):\n        lp = linkpred.LinkPred(self.config_file(training=True))\n        lp.preprocess()\n        assert set(lp.training.nodes()) == set(\"AB\")\n\n    def test_preprocess_training_and_test(self):\n        lp = linkpred.LinkPred(self.config_file(training=True, test=True))\n        lp.preprocess()\n        assert set(lp.training.nodes()) == {\"B\"}\n        assert set(lp.test.nodes()) == {\"B\"}\n\n    def test_setup_output_evaluating_without_test(self):\n        lp = linkpred.LinkPred(self.config_file(training=True))\n        with pytest.raises(linkpred.exceptions.LinkPredError):\n            lp.setup_output()\n\n    def test_setup_output(self):\n        # Make sure this also works is $DISPLAY is not set.\n        # Should probably mock this out...\n\n        for name, klass in (\n            (\"recall-precision\", RecallPrecisionPlotter),\n            (\"f-score\", FScorePlotter),\n            # Should be able to handle uppercase\n            (\"ROC\", ROCPlotter),\n            (\"fmax\", FMaxListener),\n            (\"cache-evaluations\", CacheEvaluationListener),\n        ):\n            config = self.config_file(training=True, test=True, output=[name])\n            lp = linkpred.LinkPred(config)\n            lp.setup_output()\n            assert isinstance(lp.listeners[0], klass)\n            smokesignal.clear_all()\n        # Has an evaluator been set up?\n        assert len(lp.evaluator.params[\"relevant\"]) == 1\n        assert lp.evaluator.params[\"universe\"] == 2\n        assert isinstance(lp.evaluator.params[\"universe\"], int)\n\n    def test_predict_all(self):\n        # Mock out linkpred.predictors\n        class Stub:\n            def __init__(self, training, eligible, excluded):\n                self.training = training\n                self.eligible = eligible\n                self.excluded = excluded\n\n            def predict(self, **params):\n                self.params = params\n                return \"scoresheet\"\n\n        linkpred.predictors.A = Stub\n        linkpred.predictors.B = Stub\n\n        config = self.config_file(training=True)\n        config[\"predictors\"] = [\n            {\"name\": \"A\", \"parameters\": {\"X\": \"x\"}, \"displayname\": \"prettyA\"},\n            {\"name\": \"B\", \"displayname\": \"prettyB\"},\n        ]\n        lp = linkpred.LinkPred(config)\n        results = list(lp.predict_all())\n        assert results == [(\"A\", \"scoresheet\"), (\"B\", \"scoresheet\")]\n\n    def test_process_predictions(self):\n        @smokesignal.on(\"prediction_finished\")\n        def a(scoresheet, dataset, predictor):\n            assert scoresheet.startswith(\"scoresheet\")\n            assert predictor.startswith(\"pred\")\n            assert dataset == \"testing\"\n            a.called = True\n\n        @smokesignal.on(\"dataset_finished\")\n        def b(dataset):\n            assert dataset == \"testing\"\n            b.called = True\n\n        @smokesignal.on(\"run_finished\")\n        def c():\n            c.called = True\n\n        a.called = b.called = c.called = False\n        lp = linkpred.LinkPred(self.config_file())\n        lp.predictions = [(\"pred1\", \"scoresheet1\"), (\"pred2\", \"scoresheet2\")]\n        lp.process_predictions()\n        assert a.called\n        assert b.called\n        assert c.called\n        smokesignal.clear_all()\n"
  },
  {
    "path": "tests/test_listeners.py",
    "content": "import os\nimport re\n\nimport smokesignal\n\nfrom linkpred.evaluation import BaseScoresheet, EvaluationSheet\nfrom linkpred.evaluation.listeners import (\n    CacheEvaluationListener,\n    CachePredictionListener,\n    EvaluatingListener,\n    _timestamped_filename,\n)\n\nfrom .utils import assert_array_equal\n\n\ndef test_timestamped_filename():\n    fname = _timestamped_filename(\"test\")\n    assert re.match(r\"test_\\d{4}-\\d{2}-\\d{2}_\\d{2}.\\d{2}.txt\", fname)\n    fname = _timestamped_filename(\"test\", \"foo\")\n    assert re.match(r\"test_\\d{4}-\\d{2}-\\d{2}_\\d{2}.\\d{2}.foo\", fname)\n\n\ndef test_EvaluatingListener():\n    @smokesignal.on(\"evaluation_finished\")\n    def t(evaluation, dataset, predictor):\n        assert dataset == \"dataset\"\n        assert isinstance(evaluation, EvaluationSheet)\n        assert_array_equal(evaluation.tp, [1, 1, 2, 2])\n        assert_array_equal(evaluation.fp, [0, 1, 1, 2])\n        assert_array_equal(evaluation.fn, [1, 1, 0, 0])\n        assert_array_equal(evaluation.tn, [2, 1, 1, 0])\n        assert predictor == \"predictor\"\n        t.called = True\n\n    t.called = False\n    relevant = {1, 2}\n    universe = {1, 2, 3, 4}\n    scoresheet = BaseScoresheet({1: 10, 3: 5, 2: 2, 4: 1})\n    EvaluatingListener(relevant=relevant, universe=universe)\n    smokesignal.emit(\n        \"prediction_finished\",\n        scoresheet=scoresheet,\n        dataset=\"dataset\",\n        predictor=\"predictor\",\n    )\n    assert t.called\n    smokesignal.clear_all()\n\n\ndef test_CachePredictionListener():\n    l = CachePredictionListener()\n    scoresheet = BaseScoresheet({1: 10, 2: 5, 3: 2, 4: 1})\n    smokesignal.emit(\"prediction_finished\", scoresheet, \"d\", \"p\")\n\n    with open(l.fname) as fh:\n        # Line endings may be different across platforms\n        assert fh.read().replace(\"\\r\\n\", \"\\n\") == \"1\\t10\\n2\\t5\\n3\\t2\\n4\\t1\\n\"\n    smokesignal.clear_all()\n    os.unlink(l.fname)\n\n\ndef test_CacheEvaluationListener():\n    l = CacheEvaluationListener()\n    scores = BaseScoresheet({1: 10, 2: 5})\n    ev = EvaluationSheet(scores, {1})\n    smokesignal.emit(\"evaluation_finished\", ev, \"d\", \"p\")\n\n    ev2 = EvaluationSheet.from_file(l.fname)\n    assert_array_equal(ev.data, ev2.data)\n    smokesignal.clear_all()\n    os.unlink(l.fname)\n"
  },
  {
    "path": "tests/test_predictors_base.py",
    "content": "import networkx as nx\n\nfrom linkpred.evaluation import Pair\nfrom linkpred.predictors import CommonNeighbours, Copy, Predictor, all_predictors\n\n\ndef test_bipartite_common_neighbour():\n    B = nx.Graph()\n    B.add_nodes_from(range(1, 5), eligible=0)\n    B.add_nodes_from(\"abc\", eligible=1)\n    B.add_edges_from(\n        [(1, \"a\"), (1, \"b\"), (2, \"a\"), (2, \"b\"), (2, \"c\"), (3, \"c\"), (4, \"a\")]\n    )\n\n    expected = {Pair(\"a\", \"b\"): 2, Pair(\"b\", \"c\"): 1, Pair(\"a\", \"c\"): 1}\n    assert CommonNeighbours(B, eligible=\"eligible\").predict() == expected\n\n\ndef test_bipartite_common_neighbours_equivalent_projection():\n    B = nx.bipartite.random_graph(30, 50, 0.1)\n    nodes = [v for v in B if B.nodes[v][\"bipartite\"]]\n    G = nx.bipartite.weighted_projected_graph(B, nodes)\n\n    expected = CommonNeighbours(B, eligible=\"bipartite\")()\n    assert Copy(G).predict(weight=\"weight\") == expected\n\n\ndef test_postprocessing():\n    G = nx.karate_club_graph()\n    prediction_all_links = CommonNeighbours(G)()\n    prediction_only_new_links = CommonNeighbours(G, excluded=G.edges())()\n\n    for link, score in prediction_all_links.items():\n        if G.has_edge(*link):\n            assert link not in prediction_only_new_links\n        else:\n            assert score == prediction_only_new_links[link]\n\n\ndef test_all_predictors():\n    predlist = all_predictors()\n    assert len(predlist) > 0\n    for p in predlist:\n        assert p.__base__ == Predictor\n"
  },
  {
    "path": "tests/test_predictors_eigenvector.py",
    "content": "import networkx as nx\n\nfrom linkpred.predictors.eigenvector import RootedPageRank, SimRank\n\n\nclass TestEigenvectorRuns:\n    \"\"\"Test if eigenvector methods run\n\n    This is a bit of a temporary hack to avoid regressions.We're not quite\n    sure of the correct output of, especially, SimRank, and so having tests\n    that at least run the code are better than nothing.\n\n    \"\"\"\n\n    def setup_method(self):\n        self.n = 20\n        self.G = nx.gnm_random_graph(self.n, self.n * 3)\n\n    def test_rooted_pagerank_runs(self):\n        pred = RootedPageRank(self.G).predict()\n        assert len(pred) <= self.n * (self.n - 1) // 2\n\n    def test_simrank_runs(self):\n        pred = SimRank(self.G).predict()\n        assert len(pred) == self.n * (self.n - 1) // 2\n\n\nclass TestEigenVector:\n    def test_rooted_pagerank(self):\n        pass\n\n    def test_rooted_pagerank_weighted(self):\n        pass\n\n    def test_rooted_pagerank_alpha(self):\n        pass\n\n    def test_rooted_pagerank_beta(self):\n        pass\n\n    def test_rooted_pagerank_k(self):\n        pass\n\n    def test_simrank(self):\n        pass\n\n    def test_simrank_c(self):\n        pass\n\n    def test_simrank_weighted(self):\n        pass\n"
  },
  {
    "path": "tests/test_predictors_misc.py",
    "content": "import networkx as nx\n\nfrom linkpred.evaluation import Pair\nfrom linkpred.predictors.misc import Community, Copy, Random\n\n\nclass TestCopy:\n    def setup_method(self):\n        self.G = nx.Graph()\n        self.G.add_weighted_edges_from([(0, 1, 3.0), (1, 2, 7.5)])\n\n    def test_copy_unweighted(self):\n        expected = {Pair(0, 1): 1, Pair(1, 2): 1}\n        assert Copy(self.G).predict() == expected\n\n    def test_copy_weighted(self):\n        expected = {Pair(0, 1): 3.0, Pair(1, 2): 7.5}\n        assert Copy(self.G).predict(weight=\"weight\") == expected\n\n\ndef test_community():\n    G = nx.erdos_renyi_graph(20, 0.1)\n    prediction = Community(G).predict()\n    assert len(prediction) <= 190\n\n\ndef test_community_exclude_noneligible():\n    G = nx.erdos_renyi_graph(20, 0.1)\n    G.add_nodes_from(range(10), eligible=True)\n    G.add_nodes_from(range(10, 20), eligible=False)\n\n    prediction = Community(G, eligible=\"eligible\").predict()\n    assert len(prediction) <= 45\n    for pair in prediction:\n        for v in pair:\n            assert v in range(10)\n\n\ndef test_random():\n    G = nx.Graph()\n    G.add_nodes_from(range(10), eligible=True)\n    prediction = Random(G).predict()\n    assert len(prediction) == 45\n\n\ndef test_random_exclude_noneligible():\n    G = nx.Graph()\n    G.add_nodes_from(range(5), eligible=True)\n    G.add_nodes_from(range(5, 10), eligible=False)\n    prediction = Random(G, eligible=\"eligible\").predict()\n    assert len(prediction) == 10\n    for i in range(5):\n        for j in range(5):\n            if i != j:\n                assert Pair(i, j) in prediction\n"
  },
  {
    "path": "tests/test_predictors_neighbour.py",
    "content": "from math import log, sqrt\n\nimport networkx as nx\nimport pytest\n\nimport linkpred.predictors.neighbour as nbr\nfrom linkpred.evaluation import Scoresheet\n\n\nclass TestUnweighted:\n    def setup_method(self):\n        self.G = nx.Graph()\n        self.G.add_edges_from([(1, 2), (1, 3), (2, 4), (3, 4), (3, 5)])\n\n    def test_adamic_adar(self):\n        known = {\n            (1, 5): 1 / log(3),\n            (2, 3): 2 / log(2),\n            (1, 4): 1 / log(2) + 1 / log(3),\n            (4, 5): 1 / log(3),\n        }\n        found = nbr.AdamicAdar(self.G).predict()\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_association_strength(self):\n        known = {(1, 5): 0.5, (2, 3): 1 / 3, (1, 4): 0.5, (4, 5): 0.5}\n        found = nbr.AssociationStrength(self.G).predict()\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_common_neighbours(self):\n        known = {(1, 5): 1, (2, 3): 2, (1, 4): 2, (4, 5): 1}\n        found = nbr.CommonNeighbours(self.G).predict()\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_cosine(self):\n        known = {\n            (1, 5): 1 / sqrt(2),\n            (2, 3): 2 / sqrt(6),\n            (1, 4): 1,\n            (4, 5): 1 / sqrt(2),\n        }\n        found = nbr.Cosine(self.G).predict()\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_degree_product(self):\n        known = {\n            (1, 2): 4,\n            (1, 3): 6,\n            (1, 4): 4,\n            (1, 5): 2,\n            (2, 3): 6,\n            (2, 4): 4,\n            (2, 5): 2,\n            (3, 4): 6,\n            (3, 5): 3,\n            (4, 5): 2,\n        }\n        found = nbr.DegreeProduct(self.G).predict()\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_jaccard(self):\n        known = {(1, 5): 0.5, (2, 3): 2 / 3, (1, 4): 1, (4, 5): 0.5}\n        found = nbr.Jaccard(self.G).predict()\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_nmeasure(self):\n        known = {\n            (1, 5): sqrt(2 / 5),\n            (2, 3): sqrt(8 / 13),\n            (1, 4): 1,\n            (4, 5): sqrt(2 / 5),\n        }\n        found = nbr.NMeasure(self.G).predict()\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_maxoverlap(self):\n        known = {(1, 5): 0.5, (2, 3): 2 / 3, (1, 4): 1, (4, 5): 0.5}\n        found = nbr.MaxOverlap(self.G).predict()\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_minoverlap(self):\n        known = {(1, 5): 1, (2, 3): 1, (1, 4): 1, (4, 5): 1}\n        found = nbr.MinOverlap(self.G).predict()\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_pearson(self):\n        known = {(1, 5): 0.61237243, (2, 3): 2 / 3, (1, 4): 1, (4, 5): 0.61237243}\n        found = nbr.Pearson(self.G).predict()\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_resource_allocation(self):\n        known = {(1, 5): 1 / 3, (2, 3): 1, (1, 4): 5 / 6, (4, 5): 1 / 3}\n        found = nbr.ResourceAllocation(self.G).predict()\n        assert found == pytest.approx(Scoresheet(known))\n\n\nclass TestWeighted:\n    def setup_method(self):\n        self.G = nx.Graph()\n        self.G.add_weighted_edges_from(\n            [(1, 2, 1), (1, 3, 5), (2, 4, 2), (3, 4, 1), (3, 5, 2)]\n        )\n\n    def test_adamic_adar(self):\n        known = {\n            (1, 5): 10 / log(30),\n            (1, 4): 2 / log(5) + 5 / log(30),\n            (2, 3): 2 / log(5) + 5 / log(26),\n            (4, 5): 2 / log(30),\n        }\n        found = nbr.AdamicAdar(self.G).predict(weight=\"weight\")\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_association_strength(self):\n        known = {(1, 5): 5 / 52, (2, 3): 7 / 150, (1, 4): 7 / 130, (4, 5): 0.1}\n        found = nbr.AssociationStrength(self.G).predict(weight=\"weight\")\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_common_neighbours(self):\n        known = {(1, 5): 10, (2, 3): 7, (1, 4): 7, (4, 5): 2}\n        found = nbr.CommonNeighbours(self.G).predict(weight=\"weight\")\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_cosine(self):\n        known = {\n            (1, 5): 10 / sqrt(104),\n            (2, 3): 7 / sqrt(150),\n            (1, 4): 7 / sqrt(130),\n            (4, 5): 2 / sqrt(20),\n        }\n        found = nbr.Cosine(self.G).predict(weight=\"weight\")\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_degree_product(self):\n        known = {\n            (1, 2): 130,\n            (1, 3): 780,\n            (1, 4): 130,\n            (1, 5): 104,\n            (2, 3): 150,\n            (2, 4): 25,\n            (2, 5): 20,\n            (3, 4): 150,\n            (3, 5): 120,\n            (4, 5): 20,\n        }\n        found = nbr.DegreeProduct(self.G).predict(weight=\"weight\")\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_jaccard(self):\n        known = {(1, 5): 0.5, (2, 3): 7 / 28, (1, 4): 7 / 24, (4, 5): 2 / 7}\n        found = nbr.Jaccard(self.G).predict(weight=\"weight\")\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_nmeasure(self):\n        known = {\n            (1, 5): sqrt(50 / 173),\n            (2, 3): sqrt(98 / 925),\n            (1, 4): sqrt(98 / 701),\n            (4, 5): sqrt(8 / 41),\n        }\n        found = nbr.NMeasure(self.G).predict(weight=\"weight\")\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_maxoverlap(self):\n        known = {(1, 5): 5 / 13, (2, 3): 7 / 30, (1, 4): 7 / 26, (4, 5): 0.4}\n        found = nbr.MaxOverlap(self.G).predict(weight=\"weight\")\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_minoverlap(self):\n        known = {(1, 5): 2.5, (2, 3): 1.4, (1, 4): 1.4, (4, 5): 0.5}\n        found = nbr.MinOverlap(self.G).predict(weight=\"weight\")\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_pearson(self):\n        known = {(1, 5): 0.9798502, (2, 3): 0.2965401, (1, 4): 0.4383540, (4, 5): 0.25}\n        found = nbr.Pearson(self.G).predict(weight=\"weight\")\n        assert found == pytest.approx(Scoresheet(known))\n\n    def test_resource_allocation(self):\n        known = {(1, 5): 1 / 3, (2, 3): 77 / 130, (1, 4): 17 / 30, (4, 5): 1 / 15}\n        found = nbr.ResourceAllocation(self.G).predict(weight=\"weight\")\n        assert found == pytest.approx(Scoresheet(known))\n"
  },
  {
    "path": "tests/test_predictors_path.py",
    "content": "import networkx as nx\nimport numpy as np\nimport pytest\n\nfrom linkpred.evaluation import Scoresheet\nfrom linkpred.predictors.path import GraphDistance, Katz\n\n\ndef test_katz():\n    G = nx.Graph()\n    G.add_weighted_edges_from(\n        [(1, 2, 1), (0, 2, 5), (2, 3, 1), (0, 4, 2), (1, 4, 1), (3, 5, 1), (4, 5, 3)]\n    )\n\n    beta = 0.01\n    I = np.identity(6)\n    for weight in (\"weight\", None):\n        katz = Katz(G).predict(beta=beta, weight=weight)\n\n        nodes = list(G.nodes())\n        M = nx.to_numpy_array(G, nodelist=nodes, weight=weight)\n        K = np.linalg.matrix_power(I - beta * M, -1) - I\n\n        x, y = np.asarray(K).nonzero()\n        for i, j in zip(x, y):\n            if i == j:\n                continue\n            u, v = nodes[i], nodes[j]\n            assert K[i, j] == pytest.approx(katz[(u, v)], abs=1e-5)\n\n\nclass TestGraphDistance:\n    def setup_method(self):\n        self.G = nx.Graph()\n        self.G.add_weighted_edges_from(\n            [(0, 1, 1), (0, 2, 3), (1, 2, 1), (1, 3, 2), (2, 4, 1)]\n        )\n\n    def test_unweighted(self):\n        known = {\n            (0, 1): 1,\n            (0, 2): 1,\n            (1, 2): 1,\n            (1, 3): 1,\n            (2, 4): 1,\n            (0, 3): 0.5,\n            (0, 4): 0.5,\n            (1, 4): 0.5,\n            (2, 3): 0.5,\n            (3, 4): 1 / 3,\n        }\n        known = Scoresheet(known)\n        graph_distance = GraphDistance(self.G).predict(weight=None)\n        assert graph_distance == pytest.approx(known)\n\n        graph_distance = GraphDistance(self.G).predict(alpha=0)\n        assert graph_distance == pytest.approx(known)\n\n    def test_weighted(self):\n        known = {\n            (0, 1): 1,\n            (0, 2): 3,\n            (1, 2): 1,\n            (1, 3): 2,\n            (2, 4): 1,\n            (0, 3): 2 / 3,\n            (0, 4): 0.75,\n            (1, 4): 0.5,\n            (2, 3): 2 / 3,\n            (3, 4): 0.4,\n        }\n        known = Scoresheet(known)\n        graph_distance = GraphDistance(self.G).predict()\n        assert graph_distance == pytest.approx(known)\n\n    def test_weighted_alpha(self):\n        from math import sqrt\n\n        known = {\n            (0, 1): 1,\n            (0, 2): sqrt(3),\n            (1, 2): 1,\n            (1, 3): sqrt(2),\n            (2, 4): 1,\n            (0, 3): 1 / (1 + 1 / sqrt(2)),\n            (0, 4): 1 / (1 + 1 / sqrt(3)),\n            (1, 4): 0.5,\n            (2, 3): 1 / (1 + 1 / sqrt(2)),\n            (3, 4): 1 / (2 + 1 / sqrt(2)),\n        }\n        known = Scoresheet(known)\n        graph_distance = GraphDistance(self.G).predict(alpha=0.5)\n        assert graph_distance == pytest.approx(known)\n"
  },
  {
    "path": "tests/test_preprocess.py",
    "content": "import networkx as nx\n\nfrom linkpred.preprocess import (\n    without_low_degree_nodes,\n    without_selfloops,\n    without_uncommon_nodes,\n)\n\n\ndef test_without_uncommon_nodes():\n    G1 = nx.erdos_renyi_graph(50, 0.1)\n    G2 = nx.erdos_renyi_graph(50, 0.1)\n    G1, G2 = without_uncommon_nodes([G1, G2])\n    assert len(G1) <= 50\n    assert len(G2) == len(G1)\n\n    node_sets = [range(5), range(1, 7)]\n    graphs = []\n    for nodes in node_sets:\n        G = nx.Graph()\n        G.add_nodes_from(nodes)\n        for n in G:\n            G.nodes[n][\"eligible\"] = n % 2 == 0\n        graphs.append(G)\n\n    for G in without_uncommon_nodes(graphs):\n        assert sorted(n for n in G if G.nodes[n][\"eligible\"]) == [2, 4]\n\n\ndef test_without_low_degree_nodes():\n    G = nx.star_graph(4)\n    G.add_edge(1, 2)\n    G = without_low_degree_nodes(G, minimum=2)\n    assert sorted(G) == [0, 1, 2]\n\n    edges = [(0, 1), (0, 5), (2, 3), (2, 5), (4, 3)]\n    G = nx.Graph()\n    G.add_edges_from(edges)\n    for n in G:\n        G.nodes[n][\"eligible\"] = n % 2 == 0\n    G = without_low_degree_nodes(G, minimum=2)\n    assert sorted(n for n in G if G.nodes[n][\"eligible\"]) == [0, 2]\n\n\ndef test_without_selfloops():\n    G = nx.Graph()\n    G.add_edges_from([(0, 1), (1, 2), (1, 3), (2, 2)])\n    G = without_selfloops(G)\n    assert sorted(G.edges()) == [(0, 1), (1, 2), (1, 3)]\n"
  },
  {
    "path": "tests/test_scoresheet.py",
    "content": "import networkx as nx\nimport pytest\n\nfrom linkpred.evaluation.scoresheet import BaseScoresheet, Pair, Scoresheet\n\nfrom .utils import temp_file\n\n\nclass TestBaseScoresheet:\n    def setup_method(self):\n        self.scoresheet = BaseScoresheet(zip(\"abcdefghijklmnopqrstuvwx\", range(24)))\n\n    def test_ranked_items(self):\n        d = dict(self.scoresheet.ranked_items())\n        assert d == dict(self.scoresheet)\n\n        s = self.scoresheet.ranked_items()\n        assert next(s) == (\"x\", 23)\n        assert next(s) == (\"w\", 22)\n        assert next(s) == (\"v\", 21)\n\n    def test_sets_with_threshold(self):\n        threshold = 12\n        d = dict(self.scoresheet.ranked_items(threshold=threshold))\n        assert d == dict(zip(\"xwvutsrqponm\", reversed(range(12, 24))))\n\n    def test_with_too_large_threshold(self):\n        threshold = 25\n        for s in self.scoresheet.ranked_items(threshold=threshold):\n            assert len(s) < threshold\n\n    def test_top(self):\n        top = self.scoresheet.top()\n        assert top == dict(zip(\"opqrstuvwx\", range(14, 24)))\n\n        top = self.scoresheet.top(2)\n        assert top == dict(zip(\"wx\", range(22, 24)))\n\n        top = self.scoresheet.top(100)\n        assert len(top) == 24\n\n    def test_to_file_from_file(self):\n        with temp_file() as fname:\n            self.scoresheet.to_file(fname)\n\n            newsheet = BaseScoresheet.from_file(fname)\n            assert self.scoresheet == newsheet\n\n\nclass TestScoresheetFile:\n    def setup_method(self):\n        self.sheet = Scoresheet()\n        self.sheet[(\"a\", \"b\")] = 2.0\n        self.sheet[(\"b\", \"\\xe9\")] = 1.0\n        self.expected = b\"b\\ta\\t2.0\\n\\xc3\\xa9\\tb\\t1.0\\n\"\n\n    def test_to_file(self):\n        with temp_file() as fname:\n            self.sheet.to_file(fname)\n\n            with open(fname, \"rb\") as fh:\n                assert fh.read() == self.expected\n\n    def test_from_file(self):\n        with temp_file() as fname:\n            with open(fname, \"wb\") as fh:\n                fh.write(self.expected)\n\n            sheet = Scoresheet.from_file(fname)\n            assert sheet == self.sheet\n\n\ndef test_pair():\n    t = (\"a\", \"b\")\n    pair = Pair(t)\n    assert pair == Pair(*t)\n    assert pair == t\n    assert pair == Pair(\"b\", \"a\")\n    assert pair == eval(repr(pair))\n    assert str(pair) == \"b - a\"\n\n    # Test unicode (C4 87 -> latin small letter C with acute)\n    pair = Pair(\"a\", \"\\xc4\\x87\")\n    assert str(pair) == \"\\xc4\\x87 - a\"\n\n\ndef test_pair_identical_elements():\n    with pytest.raises(AssertionError):\n        Pair(\"a\", \"a\")\n\n\ndef test_pair_different_types():\n    # Should not raise an error\n    assert Pair(\"a\", 1) == Pair(1, \"a\")\n\n\ndef test_scoresheet():\n    sheet = Scoresheet()\n    t = (\"a\", \"b\")\n    sheet[t] = 5\n    assert len(sheet) == 1\n    assert list(sheet.items()) == [(Pair(\"a\", \"b\"), 5.0)]\n    assert sheet[t] == 5.0\n    del sheet[t]\n    assert len(sheet) == 0\n\n\ndef test_scoresheet_process_data():\n    t = (\"a\", \"b\")\n    d = {t: 5}\n    G = nx.Graph()\n    G.add_edge(*t, weight=5)\n    s = [(t, 5)]\n\n    for x in (d, G, s):\n        sheet = Scoresheet(x)\n        assert sheet[t] == 5.0\n"
  },
  {
    "path": "tests/test_util.py",
    "content": "import pytest\n\nimport linkpred.util as u\n\n\ndef test_all_pairs():\n    s = [1, 2, 3, 4]\n    expected = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]\n    assert sorted(u.all_pairs(s)) == expected\n\n\ndef test_load_function():\n    import os\n\n    assert u.load_function(\"os.path.join\") == os.path.join\n\n\ndef test_load_function_no_modulename():\n    with pytest.raises(ValueError):\n        u.load_function(\"join\")\n\n\ndef test_interpolate():\n    a = [10, 8, 9, 6, 6, 7, 3, 5, 6, 2, 1, 2]\n    assert u.interpolate(a) == [10, 9, 9, 7, 7, 7, 6, 6, 6, 2, 2, 2]\n\n    a = list(range(5))\n    assert u.interpolate(a) == [4] * 5\n\n\ndef test_itersubclasses():\n    class A:\n        pass\n\n    class Aa(A):\n        pass\n\n    class Ab(A):\n        pass\n\n    class Aaa(Aa):\n        pass\n\n    def name(x):\n        return x.__name__\n\n    assert list(map(name, u.itersubclasses(A))) == [\"Aa\", \"Aaa\", \"Ab\"]\n\n\n# This is silly but hey... 100% test coverage for this file :-)\ndef test_itersubclasses_from_type():\n    list(u.itersubclasses(type))\n"
  },
  {
    "path": "tests/utils.py",
    "content": "import contextlib\nimport os\nimport tempfile\n\n\ndef assert_array_equal(a1, a2):\n    try:\n        if not (a1 == a2).all():\n            raise AssertionError(f\"{a1} != {a2}\")\n    except AttributeError:  # a1 and a2 are lists or empty ndarrays\n        assert a1 == a2\n\n\n@contextlib.contextmanager\ndef temp_file(suffix=\".tmp\"):\n    fd, fname = tempfile.mkstemp(suffix)\n    yield fname\n    os.close(fd)\n    os.unlink(fname)\n"
  },
  {
    "path": "tox.ini",
    "content": "# tox (https://tox.readthedocs.io/) is a tool for running tests\n# in multiple virtualenvs. This configuration file will run the\n# test suite on all supported python versions. To use it, \"pip install tox\"\n# and then run \"tox\" from this directory.\n\n[tox]\nenvlist = py38,py39,py310,py311,py312\n\n[testenv]\ndeps =\n    pytest\n    pytest-cov\n    python-louvain\ncommands =\n    python -m pytest --cov=linkpred\n"
  }
]