logo Netzschleuder network catalog, repository, and centrifuge

History of movielens_ratings

The changes to the source code of this entry in the repository, newest first, each with the differences it made to the entry's files. They are also listed on GitLab.

[entries] Add the date when each entry was added, and remove icon_hash by Tiago de Paula Peixoto beff7c9
The dates are those of the commits that first added the entries' files
(2020-08-11 for the entries of the initial commit); renamed entries keep
the date of their first name.

Changed: none
diff --git a/entries/movielens_ratings/__init__.py b/entries/movielens_ratings/__init__.py
index d025f69..a2d5436 100644
--- a/entries/movielens_ratings/__init__.py
+++ b/entries/movielens_ratings/__init__.py
@@ -50,7 +50,7 @@ The networks have the following properties:
 tags = ['Social', 'Online', 'Preferences', 'Weighted', 'Timestamps', 'Metadata']
 url = 'https://grouplens.org/datasets/movielens/'
 citation = [('F. M. Harper, J. A. Konstan, "The MovieLens Datasets: History and Context." ACM Transactions on Interactive Intelligent Systems 5(4), 19 (2015)', 'https://doi.org/10.1145/2827872')]
-icon_hash = None
+date_added = '2026-10-28'
 upstream_license = ("MovieLens usage license (research use; no redistribution without permission)", "https://files.grouplens.org/datasets/movielens/ml-25m-README.html")
 restricted = True
 upstream_prefix = 'https://files.grouplens.org/datasets/movielens'
[entries] Prefix the properties in the descriptions with (Node), (Edge) or (Graph) by Tiago de Paula Peixoto bc3dbbd
The lists of properties at the end of the descriptions marked edge
properties with "Edge" but left node properties unmarked, or grouped
them in "Nodes: `a`, ...; `b`, ..." items, so the kind of a property
was often unclear. Every item now starts with (Node), (Edge) or (Graph),
followed by the property names, a colon and the explanation, one item
per property or group of properties; qualifications (kinds of nodes,
networks) follow the names in parentheses. Properties that were only
named got a short explanation, and a few items that named the wrong
kind of property were corrected.
diff --git a/entries/movielens_ratings/__init__.py b/entries/movielens_ratings/__init__.py
index 2910532..d025f69 100644
--- a/entries/movielens_ratings/__init__.py
+++ b/entries/movielens_ratings/__init__.py
@@ -35,13 +35,18 @@ Each rating network contains all the movies listed in its data set, including th
 
 The networks have the following properties:
 
-- Nodes: `movie_id`, the MovieLens id of the movie (-1 for the other nodes); `title`, the title of the movie, including its year of release; `genres`, the list of its genres (empty if none is listed).
-- `20m`, `25m`, `20m_tags` and `25m_tags`, nodes: `imdb_id` and `tmdb_id`, the ids of the movie in IMDb and in The Movie Database (-1 if not given).
-- Rating networks, nodes: `is_user`, whether the node is a user (otherwise a movie); `user_id`, the anonymized id of the user (-1 for movies).
-- `100k` and `1m`, nodes: `gender` ("M" or "F"), `age`, `occupation` and `zip`, the demographic information given by the user (empty for movies). In `100k`, `age` is the age in years; in `1m`, it is the lower bound of the user's age group (1 for "under 18", 18, 25, 35, 45, 50 and 56 for "56+"). In both, -1 for movies.
-- Rating networks, edges: `rating`, the rating in stars; `time`, the time of the rating, as Unix time.
-- `20m_tags` and `25m_tags`, nodes: `is_tag`, whether the node is a tag (otherwise a movie); `name`, the tag string (empty for movies).
-- `20m_tags` and `25m_tags`, edges: `user`, the anonymized id of the user who applied the tag (the same as `user_id` in the rating network of the same data set); `time`, the time when the tag was applied, as Unix time."""
+- (Node) `movie_id`: the MovieLens id of the movie (-1 for the other nodes).
+- (Node) `title`: the title of the movie, including its year of release.
+- (Node) `genres`: the list of its genres (empty if none is listed).
+- (Node) `imdb_id`, `tmdb_id` (`20m`, `25m`, `20m_tags` and `25m_tags`): the ids of the movie in IMDb and in The Movie Database (-1 if not given).
+- (Node) `is_user` (rating networks): whether the node is a user (otherwise a movie).
+- (Node) `user_id` (rating networks): the anonymized id of the user (-1 for movies).
+- (Node) `gender`, `age`, `occupation`, `zip` (`100k` and `1m`): the demographic information given by the user (empty for movies); `gender` is "M" or "F". In `100k`, `age` is the age in years; in `1m`, it is the lower bound of the user's age group (1 for "under 18", 18, 25, 35, 45, 50 and 56 for "56+"). In both, -1 for movies.
+- (Node) `is_tag` (`20m_tags` and `25m_tags`): whether the node is a tag (otherwise a movie).
+- (Node) `name` (`20m_tags` and `25m_tags`): the tag string (empty for movies).
+- (Edge) `rating` (rating networks): the rating in stars.
+- (Edge) `time`: the time of the rating (rating networks), or when the tag was applied (`20m_tags` and `25m_tags`), as Unix time.
+- (Edge) `user` (`20m_tags` and `25m_tags`): the anonymized id of the user who applied the tag (the same as `user_id` in the rating network of the same data set)."""
 tags = ['Social', 'Online', 'Preferences', 'Weighted', 'Timestamps', 'Metadata']
 url = 'https://grouplens.org/datasets/movielens/'
 citation = [('F. M. Harper, J. A. Konstan, "The MovieLens Datasets: History and Context." ACM Transactions on Interactive Intelligent Systems 5(4), 19 (2015)', 'https://doi.org/10.1145/2827872')]
[entries] Remove unneeded code from the entries by Tiago de Paula Peixoto 2c69f19
Unneeded "global files" statements, unused imports and variables,
f-strings without placeholders, duplicate keys in foodweb_little_rock
(keeping the order and the values used), and the second definition of
get_lines() in twitter_higgs. Nothing changes in the networks or
descriptions.

Changed: none
diff --git a/entries/movielens_ratings/__init__.py b/entries/movielens_ratings/__init__.py
index e7033f6..2910532 100644
--- a/entries/movielens_ratings/__init__.py
+++ b/entries/movielens_ratings/__init__.py
@@ -218,7 +218,6 @@ def read_users(name, alt, fnames):
 @coerce_props()
 @annotate()
 def parse(alts=None):
-    global files
     name = __name__.split(".")[-1]
     for fnames, alt, fmt in files:
         if alts is not None and alt not in alts:
[entries] Remove the remarks on the distribution of the data from the descriptions of beer_reviews, bluesky_failla, citibike, divvy, facebook100, goodreads, hiv_transmission, hollywood_actors, mind_news, movielens_10m_tags, movielens_32m, movielens_ratings, netflix_prize, opensky_flights, opensky_flights_full, regulondb, statsbomb, yelp_restaurants and yelp_reviews by Tiago de Paula Peixoto 8be09c8
The descriptions said whether the data may be distributed (access
tokens, restrictions, redistribution bans, take-downs); the site shows
restricted entries itself, and the descriptions now only describe the
data.
diff --git a/entries/movielens_ratings/__init__.py b/entries/movielens_ratings/__init__.py
index 607b82c..e7033f6 100644
--- a/entries/movielens_ratings/__init__.py
+++ b/entries/movielens_ratings/__init__.py
@@ -31,7 +31,7 @@ description = """Bipartite networks of the users and movies of the online movie
 - `25m`: MovieLens 25M (released in November 2019), 25,000,095 ratings of 59,047 movies by 162,541 users, made between January 1995 and November 2019.
 - `20m_tags` and `25m_tags`: the tag applications of the 20M and 25M data sets (465,564 and 1,093,360), as bipartite networks of tags and movies, where each edge is one application of a tag (a free-text word or short phrase, case-sensitive) to a movie by a user, so that the same tag applied to the same movie by several users (or several times) gives multiple edges. Only the movies with at least one tag are included.
 
-Each rating network contains all the movies listed in its data set, including those that only received tags (which have no edges). Left out are the "tag genome" relevance scores of the 20M and 25M data sets, the cross-validation splits of the 100K and 10M data sets, and the release dates and IMDb links of the 100K data set. The tags of the 10M data set are in the entry `movielens_10m_tags`, and the most recent data set (32M), which may be redistributed, is in the entry `movielens_32m`.
+Each rating network contains all the movies listed in its data set, including those that only received tags (which have no edges). Left out are the "tag genome" relevance scores of the 20M and 25M data sets, the cross-validation splits of the 100K and 10M data sets, and the release dates and IMDb links of the 100K data set. The tags of the 10M data set are in the entry `movielens_10m_tags`, and the most recent data set (32M) is in the entry `movielens_32m`.
 
 The networks have the following properties:
 
@@ -41,9 +41,7 @@ The networks have the following properties:
 - `100k` and `1m`, nodes: `gender` ("M" or "F"), `age`, `occupation` and `zip`, the demographic information given by the user (empty for movies). In `100k`, `age` is the age in years; in `1m`, it is the lower bound of the user's age group (1 for "under 18", 18, 25, 35, 45, 50 and 56 for "56+"). In both, -1 for movies.
 - Rating networks, edges: `rating`, the rating in stars; `time`, the time of the rating, as Unix time.
 - `20m_tags` and `25m_tags`, nodes: `is_tag`, whether the node is a tag (otherwise a movie); `name`, the tag string (empty for movies).
-- `20m_tags` and `25m_tags`, edges: `user`, the anonymized id of the user who applied the tag (the same as `user_id` in the rating network of the same data set); `time`, the time when the tag was applied, as Unix time.
-
-The files of this entry are available only with an access token, since the license of these MovieLens data sets forbids redistributing them without permission."""
+- `20m_tags` and `25m_tags`, edges: `user`, the anonymized id of the user who applied the tag (the same as `user_id` in the rating network of the same data set); `time`, the time when the tag was applied, as Unix time."""
 tags = ['Social', 'Online', 'Preferences', 'Weighted', 'Timestamps', 'Metadata']
 url = 'https://grouplens.org/datasets/movielens/'
 citation = [('F. M. Harper, J. A. Konstan, "The MovieLens Datasets: History and Context." ACM Transactions on Interactive Intelligent Systems 5(4), 19 (2015)', 'https://doi.org/10.1145/2827872')]
[entries] movielens_100k: rename to movielens_10m_tags by Tiago de Paula Peixoto 9813447
The entry holds the tag assignments of the MovieLens 10M data set, not
the 100K data set (which has no tags), so its name was misleading. The
cross-references in movielens_32m and movielens_ratings follow.
diff --git a/entries/movielens_ratings/__init__.py b/entries/movielens_ratings/__init__.py
index 9d516ff..607b82c 100644
--- a/entries/movielens_ratings/__init__.py
+++ b/entries/movielens_ratings/__init__.py
@@ -31,7 +31,7 @@ description = """Bipartite networks of the users and movies of the online movie
 - `25m`: MovieLens 25M (released in November 2019), 25,000,095 ratings of 59,047 movies by 162,541 users, made between January 1995 and November 2019.
 - `20m_tags` and `25m_tags`: the tag applications of the 20M and 25M data sets (465,564 and 1,093,360), as bipartite networks of tags and movies, where each edge is one application of a tag (a free-text word or short phrase, case-sensitive) to a movie by a user, so that the same tag applied to the same movie by several users (or several times) gives multiple edges. Only the movies with at least one tag are included.
 
-Each rating network contains all the movies listed in its data set, including those that only received tags (which have no edges). Left out are the "tag genome" relevance scores of the 20M and 25M data sets, the cross-validation splits of the 100K and 10M data sets, and the release dates and IMDb links of the 100K data set. The tags of the 10M data set are in the entry `movielens_100k`, and the most recent data set (32M), which may be redistributed, is in the entry `movielens_32m`.
+Each rating network contains all the movies listed in its data set, including those that only received tags (which have no edges). Left out are the "tag genome" relevance scores of the 20M and 25M data sets, the cross-validation splits of the 100K and 10M data sets, and the release dates and IMDb links of the 100K data set. The tags of the 10M data set are in the entry `movielens_10m_tags`, and the most recent data set (32M), which may be redistributed, is in the entry `movielens_32m`.
 
 The networks have the following properties:
 
@@ -67,7 +67,7 @@ files = [(('ml-100k.zip:ml-100k/u.data', 'ml-100k.zip:ml-100k/u.item',
            'ml-25m.zip:ml-25m/links.csv'), '25m_tags', None)]
 
 # The server of files.grouplens.org does not send a certificate chain that
-# can be verified against the usual trust stores (see movielens_100k). The
+# can be verified against the usual trust stores (see movielens_10m_tags). The
 # files are then obtained from pinned Internet Archive snapshots instead; in
 # both cases their content is checked against the MD5 sums published by
 # GroupLens (ml-*.zip.md5).
[entries] New entry: movielens_ratings by Tiago de Paula Peixoto 590a58e
diff --git a/entries/movielens_ratings/__init__.py b/entries/movielens_ratings/__init__.py
new file mode 100644
index 0000000..9d516ff
--- /dev/null
+++ b/entries/movielens_ratings/__init__.py
@@ -0,0 +1,296 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2020-2026 Tiago de Paula Peixoto <tiago@skewed.de>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from .. import *
+
+import hashlib
+import pandas
+
+title = "MovieLens ratings (1995-2019)"
+description = """Bipartite networks of the users and movies of the online movie recommender service MovieLens, from the MovieLens data sets released by GroupLens Research (University of Minnesota) between 1998 and 2019. In each data set, the users were selected at random among those who had rated at least 20 movies, and their ids were anonymized. Each edge is the rating that a user gave to a movie (a user rates a movie at most once).
+
+- `100k`: MovieLens 100K (released in April 1998), 100,000 ratings of 1,682 movies by 943 users, on a scale from 1 to 5 stars, made between September 1997 and April 1998 (users who had less than 20 ratings or did not give complete demographic information were removed by GroupLens).
+- `1m`: MovieLens 1M (released in February 2003), 1,000,209 ratings of 3,706 movies by 6,040 users who joined MovieLens in 2000, on a scale from 1 to 5 stars, made between April 2000 and February 2003.
+- `10m`: MovieLens 10M (released in January 2009), 10,000,054 ratings of 10,677 movies by 69,878 users, made between January 1995 and January 2009, on a scale from 0.5 to 5 stars in half-star steps (whole stars only before February 2003).
+- `20m`: MovieLens 20M (released in October 2016), 20,000,263 ratings of 26,744 movies by 138,493 users, made between January 1995 and March 2015.
+- `25m`: MovieLens 25M (released in November 2019), 25,000,095 ratings of 59,047 movies by 162,541 users, made between January 1995 and November 2019.
+- `20m_tags` and `25m_tags`: the tag applications of the 20M and 25M data sets (465,564 and 1,093,360), as bipartite networks of tags and movies, where each edge is one application of a tag (a free-text word or short phrase, case-sensitive) to a movie by a user, so that the same tag applied to the same movie by several users (or several times) gives multiple edges. Only the movies with at least one tag are included.
+
+Each rating network contains all the movies listed in its data set, including those that only received tags (which have no edges). Left out are the "tag genome" relevance scores of the 20M and 25M data sets, the cross-validation splits of the 100K and 10M data sets, and the release dates and IMDb links of the 100K data set. The tags of the 10M data set are in the entry `movielens_100k`, and the most recent data set (32M), which may be redistributed, is in the entry `movielens_32m`.
+
+The networks have the following properties:
+
+- Nodes: `movie_id`, the MovieLens id of the movie (-1 for the other nodes); `title`, the title of the movie, including its year of release; `genres`, the list of its genres (empty if none is listed).
+- `20m`, `25m`, `20m_tags` and `25m_tags`, nodes: `imdb_id` and `tmdb_id`, the ids of the movie in IMDb and in The Movie Database (-1 if not given).
+- Rating networks, nodes: `is_user`, whether the node is a user (otherwise a movie); `user_id`, the anonymized id of the user (-1 for movies).
+- `100k` and `1m`, nodes: `gender` ("M" or "F"), `age`, `occupation` and `zip`, the demographic information given by the user (empty for movies). In `100k`, `age` is the age in years; in `1m`, it is the lower bound of the user's age group (1 for "under 18", 18, 25, 35, 45, 50 and 56 for "56+"). In both, -1 for movies.
+- Rating networks, edges: `rating`, the rating in stars; `time`, the time of the rating, as Unix time.
+- `20m_tags` and `25m_tags`, nodes: `is_tag`, whether the node is a tag (otherwise a movie); `name`, the tag string (empty for movies).
+- `20m_tags` and `25m_tags`, edges: `user`, the anonymized id of the user who applied the tag (the same as `user_id` in the rating network of the same data set); `time`, the time when the tag was applied, as Unix time.
+
+The files of this entry are available only with an access token, since the license of these MovieLens data sets forbids redistributing them without permission."""
+tags = ['Social', 'Online', 'Preferences', 'Weighted', 'Timestamps', 'Metadata']
+url = 'https://grouplens.org/datasets/movielens/'
+citation = [('F. M. Harper, J. A. Konstan, "The MovieLens Datasets: History and Context." ACM Transactions on Interactive Intelligent Systems 5(4), 19 (2015)', 'https://doi.org/10.1145/2827872')]
+icon_hash = None
+upstream_license = ("MovieLens usage license (research use; no redistribution without permission)", "https://files.grouplens.org/datasets/movielens/ml-25m-README.html")
+restricted = True
+upstream_prefix = 'https://files.grouplens.org/datasets/movielens'
+files = [(('ml-100k.zip:ml-100k/u.data', 'ml-100k.zip:ml-100k/u.item',
+           'ml-100k.zip:ml-100k/u.user'), '100k', None),
+         (('ml-1m.zip:ml-1m/ratings.dat', 'ml-1m.zip:ml-1m/movies.dat',
+           'ml-1m.zip:ml-1m/users.dat'), '1m', None),
+         (('ml-10m.zip:ml-10M100K/ratings.dat', 'ml-10m.zip:ml-10M100K/movies.dat'),
+          '10m', None),
+         (('ml-20m.zip:ml-20m/ratings.csv', 'ml-20m.zip:ml-20m/movies.csv',
+           'ml-20m.zip:ml-20m/links.csv'), '20m', None),
+         (('ml-25m.zip:ml-25m/ratings.csv', 'ml-25m.zip:ml-25m/movies.csv',
+           'ml-25m.zip:ml-25m/links.csv'), '25m', None),
+         (('ml-20m.zip:ml-20m/tags.csv', 'ml-20m.zip:ml-20m/movies.csv',
+           'ml-20m.zip:ml-20m/links.csv'), '20m_tags', None),
+         (('ml-25m.zip:ml-25m/tags.csv', 'ml-25m.zip:ml-25m/movies.csv',
+           'ml-25m.zip:ml-25m/links.csv'), '25m_tags', None)]
+
+# The server of files.grouplens.org does not send a certificate chain that
+# can be verified against the usual trust stores (see movielens_100k). The
+# files are then obtained from pinned Internet Archive snapshots instead; in
+# both cases their content is checked against the MD5 sums published by
+# GroupLens (ml-*.zip.md5).
+archives = {
+    "ml-100k.zip": ("20260508013803", "0e33842e24a9c977be4e0107933c0723"),
+    "ml-1m.zip": ("20260513093508", "c4d9eecfca2ab87c1945afe126590906"),
+    "ml-10m.zip": ("20220331225744", "ce571fd55effeba0271552578f2648bd"),
+    "ml-20m.zip": ("20250126141522", "cd245b17a1ae2cc31bb14903e1204af3"),
+    "ml-25m.zip": ("20250126141557", "6b51fb2759a8657d3bfcbfc42b592ada"),
+}
+
+def fetch_upstream(force=False):
+    name = __name__.split(".")[-1]
+    def download(url):
+        with open_url(url) as f:
+            return f.read()
+    for fname, (snapshot, md5sum) in archives.items():
+        if not force:
+            try:
+                with open_upstream_file(name, fname, "rb"):
+                    continue
+            except (FileNotFoundError, IOError):
+                pass
+        try:
+            data = download(f"{upstream_prefix}/{fname}")
+        except urllib.error.URLError as e:
+            if not isinstance(e.reason, ssl.SSLCertVerificationError):
+                raise
+            data = retry_download(download, f"https://web.archive.org/web/{snapshot}id_/{upstream_prefix}/{fname}",
+                                  tries=3)
+        if hashlib.md5(data).hexdigest() != md5sum:
+            raise RuntimeError(f"unexpected content for {fname}")
+        with open_upstream_file(name, fname, "wb") as output:
+            output.write(data)
+
+OCCUPATIONS_1M = ["other", "academic/educator", "artist", "clerical/admin",
+                  "college/grad student", "customer service",
+                  "doctor/health care", "executive/managerial", "farmer",
+                  "homemaker", "K-12 student", "lawyer", "programmer",
+                  "retired", "sales/marketing", "scientist", "self-employed",
+                  "technician/engineer", "tradesman/craftsman", "unemployed",
+                  "writer"]
+
+GENRES_100K = ["unknown", "Action", "Adventure", "Animation", "Children's",
+               "Comedy", "Crime", "Documentary", "Drama", "Fantasy",
+               "Film-Noir", "Horror", "Musical", "Mystery", "Romance",
+               "Sci-Fi", "Thriller", "War", "Western"]
+
+def read_dat(name, fname, ncols, encoding="utf8"):
+    "Rows of a '::'-separated file of the 1M and 10M data sets."
+    rows = []
+    with open_upstream_file(name, fname, "r", encoding=encoding) as f:
+        for line in f:
+            line = line.rstrip("\n")
+            if line == "":
+                continue
+            row = line.split("::")
+            assert len(row) == ncols, row
+            rows.append(row)
+    return rows
+
+def read_movies(name, alt, fnames):
+    """The movies of the data set, as a data frame with columns movieId,
+    title, genres (a list) and, for the 20M and 25M data sets, imdbId and
+    tmdbId."""
+    if alt == "100k":
+        rows = []
+        with open_upstream_file(name, fnames[1], "r", encoding="latin1") as f:
+            for line in f:
+                row = line.rstrip("\n").split("|")
+                if len(row) < 24:
+                    continue
+                flags = row[5:24]
+                rows.append((int(row[0]), row[1],
+                             [g for g, x in zip(GENRES_100K, flags) if x == "1"]))
+        return pandas.DataFrame(rows, columns=["movieId", "title", "genres"])
+    if alt in ("1m", "10m"):
+        rows = read_dat(name, fnames[1], 3,
+                        encoding="latin1" if alt == "1m" else "utf8")
+        return pandas.DataFrame([(int(m), t, [] if gs == "(no genres listed)" else gs.split("|"))
+                                 for m, t, gs in rows],
+                                columns=["movieId", "title", "genres"])
+    with open_upstream_file(name, fnames[1], "rb") as f:
+        movies = pandas.read_csv(f, dtype={"movieId": "int64", "title": str,
+                                           "genres": str},
+                                 keep_default_na=False)
+    movies["genres"] = [[] if gs == "(no genres listed)" else gs.split("|")
+                        for gs in movies.genres.values]
+    with open_upstream_file(name, fnames[2], "rb") as f:
+        links = pandas.read_csv(f, dtype={"movieId": "int64"})
+    links = links.fillna(-1).astype("int64")
+    return movies.merge(links, on="movieId", how="left").fillna(-1)
+
+def set_movie_props(g, vs, movies):
+    "Set the properties of the movie nodes `vs` (indices) from the frame."
+    g.vp.movie_id = g.new_vp("int64_t", val=-1)
+    g.vp.title = g.new_vp("string")
+    g.vp.genres = g.new_vp("vector<string>")
+    g.vp.movie_id.a[vs] = movies.movieId.values
+    for v, t, gs in zip(vs, movies.title.values, movies.genres.values):
+        g.vp.title[v] = t
+        g.vp.genres[v] = gs
+    if "imdbId" in movies.columns:
+        g.vp.imdb_id = g.new_vp("int64_t", val=-1)
+        g.vp.tmdb_id = g.new_vp("int64_t", val=-1)
+        g.vp.imdb_id.a[vs] = movies.imdbId.values.astype("int64")
+        g.vp.tmdb_id.a[vs] = movies.tmdbId.values.astype("int64")
+
+def read_ratings(name, alt, fnames):
+    "The ratings, as a data frame with columns userId, movieId, rating, timestamp."
+    cols = ["userId", "movieId", "rating", "timestamp"]
+    if alt == "100k":
+        with open_upstream_file(name, fnames[0], "rb") as f:
+            return pandas.read_csv(f, sep="\t", header=None, names=cols,
+                                   dtype={"userId": "int64", "movieId": "int64",
+                                          "rating": "float64", "timestamp": "int64"})
+    if alt in ("1m", "10m"):
+        # "::" separators: read as ":" with empty columns in between
+        with open_upstream_file(name, fnames[0], "rb") as f:
+            df = pandas.read_csv(f, sep=":", header=None, usecols=[0, 2, 4, 6],
+                                 names=["userId", "a", "movieId", "b", "rating", "c",
+                                        "timestamp"],
+                                 dtype={"userId": "int64", "movieId": "int64",
+                                        "rating": "float64", "timestamp": "int64"})
+        return df[cols]
+    with open_upstream_file(name, fnames[0], "rb") as f:
+        return pandas.read_csv(f, dtype={"userId": "int64", "movieId": "int64",
+                                         "rating": "float64", "timestamp": "int64"})
+
+def read_users(name, alt, fnames):
+    """The users' demographic data of the 100K and 1M data sets, as a data
+    frame with columns userId, gender, age, occupation, zip."""
+    if alt == "100k":
+        rows = []
+        with open_upstream_file(name, fnames[2], "r", encoding="latin1") as f:
+            for line in f:
+                line = line.rstrip("\n")
+                if line == "":
+                    continue
+                u, age, gender, occ, zipc = line.split("|")
+                rows.append((int(u), gender, int(age), occ, zipc))
+    else:
+        rows = [(int(u), gender, int(age), OCCUPATIONS_1M[int(occ)], zipc)
+                for u, gender, age, occ, zipc in read_dat(name, fnames[2], 5,
+                                                          encoding="latin1")]
+    return pandas.DataFrame(rows, columns=["userId", "gender", "age",
+                                           "occupation", "zip"])
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+    global files
+    name = __name__.split(".")[-1]
+    for fnames, alt, fmt in files:
+        if alts is not None and alt not in alts:
+            continue
+        movies = read_movies(name, alt.removesuffix("_tags"), fnames)
+        mpos = pandas.Index(movies.movieId.values)
+        assert mpos.is_unique
+        if not alt.endswith("_tags"):
+            df = read_ratings(name, alt, fnames)
+            if alt in ("100k", "1m"):
+                users = read_users(name, alt, fnames)
+                assert users.userId.is_unique
+                assert set(df.userId.values) <= set(users.userId.values)
+                users = users.sort_values("userId")
+                uids = users.userId.values
+            else:
+                uids = numpy.unique(df.userId.values)
+            N_u = len(uids)
+            g = Graph(directed=False)
+            g.add_vertex(N_u + len(movies))
+            g.vp.is_user = g.new_vp("bool")
+            g.vp.is_user.a[:N_u] = True
+            g.vp.user_id = g.new_vp("int64_t", val=-1)
+            g.vp.user_id.a[:N_u] = uids
+            if alt in ("100k", "1m"):
+                g.vp.gender = g.new_vp("string")
+                g.vp.age = g.new_vp("int", val=-1)
+                g.vp.occupation = g.new_vp("string")
+                g.vp.zip = g.new_vp("string")
+                g.vp.age.a[:N_u] = users.age.values
+                for v, (gd, oc, z) in enumerate(zip(users.gender.values,
+                                                    users.occupation.values,
+                                                    users.zip.values)):
+                    g.vp.gender[v] = gd
+                    g.vp.occupation[v] = oc
+                    g.vp.zip[v] = z
+            set_movie_props(g, numpy.arange(N_u, N_u + len(movies)), movies)
+            u = numpy.searchsorted(uids, df.userId.values)
+            m = mpos.get_indexer(df.movieId.values)
+            assert (m >= 0).all()
+            assert not pandas.Series(df.userId.values * (int(movies.movieId.max()) + 1)
+                                     + df.movieId.values).duplicated().any()
+            g.ep.rating = g.new_ep("double")
+            g.ep.time = g.new_ep("int64_t")
+            g.add_edge_list(numpy.column_stack((u, N_u + m)))
+            g.ep.rating.a = df.rating.values
+            g.ep.time.a = df.timestamp.values
+            del df
+        else:
+            with open_upstream_file(name, fnames[0], "rb") as f:
+                df = pandas.read_csv(f, dtype={"userId": "int64", "movieId": "int64",
+                                               "tag": str, "timestamp": "int64"},
+                                     keep_default_na=False)
+            tag_names, t = numpy.unique(df.tag.values.astype(str),
+                                        return_inverse=True)
+            mids, m = numpy.unique(df.movieId.values, return_inverse=True)
+            N_t = len(tag_names)
+            tmovies = movies.iloc[mpos.get_indexer(mids)]
+            assert (mpos.get_indexer(mids) >= 0).all()
+            g = Graph(directed=False)
+            g.add_vertex(N_t + len(mids))
+            g.vp.is_tag = g.new_vp("bool")
+            g.vp.is_tag.a[:N_t] = True
+            g.vp.name = g.new_vp("string")
+            for v, s in enumerate(tag_names):
+                g.vp.name[v] = s
+            set_movie_props(g, numpy.arange(N_t, N_t + len(mids)), tmovies)
+            g.ep.user = g.new_ep("int64_t")
+            g.ep.time = g.new_ep("int64_t")
+            g.add_edge_list(numpy.column_stack((t, N_t + m)))
+            g.ep.user.a = df.userId.values
+            g.ep.time.a = df.timestamp.values
+        yield alt, g