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