History of movielens_10m_tags
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] 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 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_10m_tags/__init__.py b/entries/movielens_10m_tags/__init__.py
index d585a17..922e1f0 100644
--- a/entries/movielens_10m_tags/__init__.py
+++ b/entries/movielens_10m_tags/__init__.py
@@ -26,9 +26,7 @@ description = """A bipartite network of the tags that users of the online movie
The network has the following properties:
- Nodes: `is_tag`, whether the node is a tag (otherwise it is a movie); `name`, the tag string, or the title of the movie (including its year of release, as entered in MovieLens); `movie_id`, the MovieLens movie id (-1 for tags); `genres`, the list of genres of the movie.
-- Edges: `user`, the anonymized id of the user who applied the tag; `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 MovieLens license forbids redistributing the data without permission."""
+- Edges: `user`, the anonymized id of the user who applied the tag; `time`, the time when the tag was applied, as Unix time."""
tags = ['Informational', 'Folksonomy', 'Unweighted', 'Multigraph', 'Timestamps', 'Metadata']
url = 'https://grouplens.org/datasets/movielens/10m/'
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 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_10m_tags/__init__.py b/entries/movielens_10m_tags/__init__.py
new file mode 100644
index 0000000..d585a17
--- /dev/null
+++ b/entries/movielens_10m_tags/__init__.py
@@ -0,0 +1,115 @@
+#! /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
+
+title = "MovieLens 10M tag assignments (2005-2009)"
+description = """A bipartite network of the tags that users of the online movie recommender service MovieLens applied to movies, from the MovieLens 10M data set released by GroupLens Research (University of Minnesota) in January 2009. MovieLens users can annotate movies with free-text tags, typically a single word or short phrase whose meaning is determined by the user. The data set contains 95,580 tag applications made between December 2005 and January 2009 by 4,009 users, who were selected at random (all selected users had rated at least 20 movies) and anonymized. Each node is a tag (a distinct tag string, case-sensitive) or a movie, and each edge is one application of the tag to the movie by some user, so that the same tag applied to the same movie by several users appears as multiple edges. Only movies that received at least one tag are included.
+
+The network has the following properties:
+
+- Nodes: `is_tag`, whether the node is a tag (otherwise it is a movie); `name`, the tag string, or the title of the movie (including its year of release, as entered in MovieLens); `movie_id`, the MovieLens movie id (-1 for tags); `genres`, the list of genres of the movie.
+- Edges: `user`, the anonymized id of the user who applied the tag; `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 MovieLens license forbids redistributing the data without permission."""
+tags = ['Informational', 'Folksonomy', 'Unweighted', 'Multigraph', 'Timestamps', 'Metadata']
+url = 'https://grouplens.org/datasets/movielens/10m/'
+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 = '5759d552bd3e93b53c695600'
+upstream_license = ("MovieLens usage license (research use; no redistribution without permission)", "https://files.grouplens.org/datasets/movielens/ml-10m-README.html")
+restricted = True
+upstream_prefix = 'https://files.grouplens.org/datasets/movielens'
+files = [(('ml-10m.zip:ml-10M100K/tags.dat', 'ml-10m.zip:ml-10M100K/movies.dat'),
+ None, None)]
+
+# The server of files.grouplens.org does not send a certificate chain that
+# can be verified against the usual trust stores (it ends in an emSign root
+# that is missing from them). The file is then obtained from a pinned
+# Internet Archive snapshot instead; in both cases its content is checked
+# against the MD5 sum published by GroupLens.
+archived_url = 'https://web.archive.org/web/20220331225744id_/https://files.grouplens.org/datasets/movielens/ml-10m.zip'
+md5sum = 'ce571fd55effeba0271552578f2648bd'
+
+def fetch_upstream(force=False):
+ name = __name__.split(".")[-1]
+ if not force:
+ try:
+ with open_upstream_file(name, "ml-10m.zip", "rb"):
+ return
+ except (FileNotFoundError, IOError):
+ pass
+ def download(url):
+ with open_url(url) as f:
+ return f.read()
+ try:
+ data = download(f"{upstream_prefix}/ml-10m.zip")
+ except urllib.error.URLError as e:
+ if not isinstance(e.reason, ssl.SSLCertVerificationError):
+ raise
+ data = retry_download(download, archived_url, tries=3)
+ if hashlib.md5(data).hexdigest() != md5sum:
+ raise RuntimeError("unexpected content for ml-10m.zip")
+ with open_upstream_file(name, "ml-10m.zip", "wb") as output:
+ output.write(data)
+
+@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
+ with open_upstream_file(name, fnames[0], "r") as f:
+ # UserID::MovieID::Tag::Timestamp
+ rows = [line.rstrip("\n").split("::") for line in f if line.strip()]
+ movies = {}
+ with open_upstream_file(name, fnames[1], "r") as f:
+ # MovieID::Title::Genres
+ for line in f:
+ if not line.strip():
+ continue
+ mid, mtitle, genres = line.rstrip("\n").split("::")
+ movies[int(mid)] = (mtitle, genres.split("|"))
+ g = Graph(directed=False)
+ g.vp.is_tag = g.new_vp("bool")
+ g.vp.name = g.new_vp("string")
+ g.vp.movie_id = g.new_vp("int")
+ g.vp.genres = g.new_vp("vector<string>")
+ g.ep.user = g.new_ep("int")
+ g.ep.time = g.new_ep("int64_t")
+ tag_v = {}
+ movie_v = {}
+ elist = []
+ for user, mid, tag, t in rows:
+ if tag not in tag_v:
+ v = tag_v[tag] = g.add_vertex()
+ g.vp.is_tag[v] = True
+ g.vp.name[v] = tag
+ g.vp.movie_id[v] = -1
+ mid = int(mid)
+ if mid not in movie_v:
+ v = movie_v[mid] = g.add_vertex()
+ g.vp.movie_id[v] = mid
+ g.vp.name[v], g.vp.genres[v] = movies[mid]
+ elist.append((int(tag_v[tag]), int(movie_v[mid]), int(user), int(t)))
+ g.add_edge_list(elist, eprops=[g.ep.user, g.ep.time])
+ yield alt, g