pageviews: Séparation script/module & session

This commit is contained in:
Mattéo Delabre 2019-11-26 00:43:40 -05:00
parent 5f7a3ddf69
commit adba50d786
Signed by: matteo
GPG Key ID: AE3FBD02DC583ABB
14 changed files with 394 additions and 7717 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules
__pycache__
.cache
dist

0
data/fetch/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

18
data/fetch/http.py Normal file
View File

@ -0,0 +1,18 @@
import requests
import urllib3
# Session de requêtage HTTP
session = requests.Session()
# Réalise 10 essais de récupération de chaque page, en augmentant
# progressivement le délai entre chaque essai
retry = urllib3.util.Retry(total=10, backoff_factor=0.3)
adapter = requests.adapters.HTTPAdapter(max_retries=retry)
session.mount('http', adapter)
session.mount('https', adapter)
# Identification du robot
session.headers['User-Agent'] = (
'WikimedicaDiseaseSearch/0.1 '
'(https://gitlab.com/matteodelabre/wikimedica-disease-search)'
)

View File

@ -0,0 +1,56 @@
from .http import session
import mwclient
def instanciate(site):
"""
Instancie un objet permettant daccéder à lAPI dun site MediaWiki.
:param site: Nom de domaine du site.
:return: Instance mwclient.Site pour le site.
"""
return mwclient.Site(site, pool=session)
def article_canonical(site, article):
"""
Obtient le nom canonique dun article après avoir suivi les redirections.
:param site: Instance mwclient.Site pour le site ciblé.
:param article: Article ciblé dans le site.
:return: Nom canonique de larticle.
"""
original_page = site.pages[article]
if not original_page.exists:
raise Exception(
'Article « {} » inexistant sur {}'
.format(article, site.host)
)
return original_page.resolve_redirect().name
def article_redirects(site, article):
"""
Récupère lensemble des pages redirigeant vers un article.
:param site: Instance mwclient.Site pour le site ciblé.
:param article: Article ciblé dans le site.
:return: Liste des pages redirigeant vers `article`.
"""
response = site.api('query', prop='redirects', titles=article)
page_response = list(response['query']['pages'].values())[0]
if 'missing' in page_response:
raise Exception(
'Article « {} » inexistant sur {}'
.format(article, site.host)
)
# Si la réponse ninclut pas la clé «redirects», cest quil nexiste
# aucune redirection vers la page
return [
item['title'] for item in
page_response['redirects']
] if 'redirects' in page_response else []

View File

@ -0,0 +1,137 @@
import collections
from datetime import datetime
from .http import session
import numpy
from scipy import stats
# Chemin racine pour les API Wikimedia
wikimedia_base_path = 'https://wikimedia.org/api/rest_v1'
# Patron daccès à lAPI pageviews de Wikimedia
wikimedia_pageviews_path = '/' + '/'.join([
'metrics', 'pageviews', 'per-article', '{project}',
'{access}', '{agent}', '{article}', '{granularity}',
'{start}', '{end}'
])
# Format de dates utilisée pour lAPI Wikimedia
wikimedia_date_format = '%Y%m%d'
# Date de première disponibilité des pageviews sur lAPI Wikimedia
wikimedia_pageviews_start = datetime(2015, 7, 1)
# Tableau contenant tous les jours de lannée de 1 à 365
year_all_days = numpy.arange(1, 366)
def _year_date_distance(a, b):
"""
Calcule la distance entre deux jours de lannée.
:example: _year_date_distance(10, 360) == 15
:example: _year_date_distance(numpy.array([10, 182, 355]), 182)
== [172, 0, 173]
:param a: Première valeur (peut être un tableau numpy).
:param b: Seconde valeur (peut être un tableau numpy).
:return: Valeur de la distance.
"""
return numpy.stack((
numpy.mod(a - b, len(year_all_days)),
numpy.mod(b - a, len(year_all_days))
)).min(axis=0)
def smooth(views, scale):
"""
Lisse un tableau de valeurs contenant une valeur par jour de lannée en
utilisant un noyau gaussien.
À la bordure de fin ou de début dannée, le lissage est fait avec lautre
morceau de lannée.
:param views: Données à lisser.
:param scale: Variance du noyau gaussien.
:return: Données lissées.
"""
ref_pdf = stats.norm.pdf(
_year_date_distance(year_all_days, 1),
scale=scale
)
pdf_matrix = numpy.stack([
numpy.roll(ref_pdf, day - 1)
for day in year_all_days
])
return pdf_matrix.dot(views)
def get(project, article):
"""
Obtient le nombre de visites sur une page Wikipédia par jour.
:param project: Projet Wikipédia ciblé.
:param article: Article ciblé dans le site.
:return: Compteur associant chaque jour à son nombre de visites.
"""
# Soumet une requête à lAPI REST pour obtenir les vues de larticle
res = session.get(wikimedia_base_path + wikimedia_pageviews_path.format(
project=project,
article=article,
access='all-access',
agent='user',
granularity='daily',
start=wikimedia_pageviews_start.strftime(wikimedia_date_format),
end=datetime.today().strftime(wikimedia_date_format)
))
data = res.json()
# Vérifie que la réponse reçue indique un succès
if res.status_code != 200:
if 'detail' in data:
detail = data['detail']
message = ', '.join(detail) if type(detail) == list else detail
raise Exception(message)
else:
raise Exception('Erreur {}'.format(res.status_code))
# Construit le dictionnaire résultant
return collections.Counter(dict(
(record['timestamp'][:8], record['views'])
for record in data['items']
))
def mean(views):
"""
Calcule les vues moyennes par jour de lannée à partir dun ensemble de
vues enregistrées (omettant le 29 février pour les années bissextiles).
:param views: Vues enregistrées par date.
:return: Tableau de taille 365 contenant le nombre de visites moyennes pour
chaque jour de lannée.
"""
# Fait la moyenne pour chaque jour hormis le 29 février
accumulator = {}
datemonth_format = '%m%d'
for day in range(1, 366):
datemonth = datetime.fromordinal(day).strftime(datemonth_format)
accumulator[datemonth] = []
for date_str, views in views.items():
date = datetime.strptime(date_str, wikimedia_date_format)
if not (date.month == 2 and date.day == 29):
datemonth = date.strftime(datemonth_format)
accumulator[datemonth].append(views)
for datemonth, value in accumulator.items():
accumulator[datemonth] = sum(value) / len(value) if value else 0
# Rassemble les valeurs moyennes pour chaque jour dans l'ordre de l'année
return [item[1] for item in sorted(
list(accumulator.items()),
key=lambda x: x[0]
)]

91
data/pageviews.py Normal file
View File

@ -0,0 +1,91 @@
import calendar
import collections
from datetime import datetime
from fetch import wikipedia_pageviews, mediawiki_api
import logging
import matplotlib
from matplotlib import pyplot
import sys
# Objet pour afficher le journal dexécution
logger = logging.getLogger('pageviews')
logging.basicConfig(level=logging.INFO)
if len(sys.argv) < 4:
print("""Utilisation: {} [project] [output] [article]...
Représente sur un graphe les statistiques moyenne de vue de pages wiki.
Paramètres:
project Projet Wikipédia ciblé (par exemple fr.wikipedia.org).
output Nom du fichier de sortie sera sauvé le graphe, ou '-' pour
afficher le résultat à lécran.
article Nom(s) darticle(s) Wikipédia ciblé(s).
Au moins un article doit être donné. Le nombre de visites est lissé avec un
noyau gaussien décart-type 10 jours. Les redirections darticle sont suivies
et toute visite sur une page de redirection pointant vers larticle est
dénombrée comme une visite sur la page canonique.
""".format(sys.argv[0]), end='', file=sys.stderr)
sys.exit(1)
project = sys.argv[1]
output = sys.argv[2]
articles = sys.argv[3:]
site = mediawiki_api.instanciate(project)
output_to_file = output != '-'
if output_to_file:
matplotlib.use('pgf')
matplotlib.rcParams.update({
'pgf.texsystem': 'xelatex',
'font.family': 'serif',
'text.usetex': True,
'pgf.rcfonts': False,
})
fig = pyplot.figure(figsize=(4.7, 3.3))
ax = fig.add_subplot(111)
ax.set_xlabel('Jours de lannée')
ax.set_xticks([
datetime(1, month, 1).toordinal()
for month in range(1, 13)
])
ax.set_xticklabels(calendar.month_abbr[1:13])
for article in articles:
canonical = mediawiki_api.article_canonical(site, article)
if article != canonical:
logger.info(
'Suivi de la redirection de « {} » en « {} »'
.format(article, canonical)
)
del article
redirects = mediawiki_api.article_redirects(site, canonical)
mean_views = wikipedia_pageviews.mean(sum(
(wikipedia_pageviews.get(project, page)
for page in redirects + [canonical]),
start=collections.Counter()
))
ax.plot(
wikipedia_pageviews.smooth(mean_views, 10),
label=canonical
)
ax.set_ylabel('Vues par jour')
fig.legend(framealpha=1)
fig.autofmt_xdate()
fig.tight_layout()
if output_to_file:
pyplot.savefig(output)
else:
pyplot.show()

View File

@ -1,282 +0,0 @@
import calendar
import collections
from datetime import datetime
import logging
import matplotlib
import numpy
from matplotlib import pyplot
import requests
from scipy import stats
import sys
import mwclient
# Chemin racine pour les API Wikimedia
wikimedia_base_path = 'https://wikimedia.org/api/rest_v1'
# Patron daccès à lAPI pageviews de Wikimedia
wikimedia_pageviews_path = '/' + '/'.join([
'metrics', 'pageviews', 'per-article', '{project}',
'{access}', '{agent}', '{article}', '{granularity}',
'{start}', '{end}'
])
# Format de dates utilisée pour lAPI Wikimedia
wikimedia_date_format = '%Y%m%d'
# Date de première disponibilité des pageviews sur lAPI Wikimedia
wikimedia_pageviews_start = datetime(2015, 7, 1)
# Objet pour afficher le journal dexécution
logger = logging.getLogger('pageviews')
# Tableau contenant tous les jours de lannée de 1 à 365
year_all_days = numpy.arange(1, 366)
def year_date_distance(a, b):
"""
Calcule la distance entre deux jours de lannée.
:example: year_date_distance(10, 360) == 15
:example: year_date_distance(numpy.array([10, 182, 355]), 182)
== [172, 0, 173]
:param a: Première valeur (peut être un tableau numpy).
:param b: Seconde valeur (peut être un tableau numpy).
:return: Valeur de la distance.
"""
return numpy.stack((
numpy.mod(a - b, len(year_all_days)),
numpy.mod(b - a, len(year_all_days))
)).min(axis=0)
def year_smooth_gaussian(data, scale):
"""
Lisse un tableau de valeurs contenant une valeur par jour de lannée en
utilisant un noyau gaussien.
À la bordure de fin ou de début dannée, le lissage est fait avec lautre
morceau de lannée.
:param data: Données à lisser.
:param scale: Variance du noyau gaussien.
:return: Données lissées.
"""
ref_pdf = stats.norm.pdf(year_date_distance(year_all_days, 1), scale=scale)
pdf_matrix = numpy.stack([
numpy.roll(ref_pdf, day - 1)
for day in year_all_days
])
return pdf_matrix.dot(data)
def wikimedia_page_views(site, article):
"""
Obtient le nombre de visites sur une page Wikipédia par jour.
:param site: Site Wikipédia ciblé.
:param article: Article ciblé dans le site.
:return: Compteur associant chaque jour à son nombre de visites.
"""
# Soumet une requête à lAPI REST pour obtenir les vues de larticle
res = requests.get(wikimedia_base_path + wikimedia_pageviews_path.format(
project=site.host,
article=article,
access='all-access',
agent='user',
granularity='daily',
start=wikimedia_pageviews_start.strftime(wikimedia_date_format),
end=datetime.today().strftime(wikimedia_date_format)
))
data = res.json()
# Vérifie que la réponse reçue indique un succès
if res.status_code != 200:
if 'detail' in data:
detail = data['detail']
message = ', '.join(detail) if type(detail) == list else detail
raise Exception(message)
else:
raise Exception('Erreur {}'.format(res.status_code))
# Construit le dictionnaire résultant
return collections.Counter(dict(
(record['timestamp'][:8], record['views'])
for record in data['items']
))
def wikimedia_article_canonical_name(site, article):
"""
Obtient le nom canonique dun article après avoir suivi les redirections.
:param site: Objet empaquetant lAPI du site Wikipédia ciblé.
:param article: Article ciblé dans le site.
:return: Nom canonique de larticle.
"""
original_page = site.pages[article]
if not original_page.exists:
raise Exception(
'Article « {} » inexistant sur {}'
.format(article, site.host)
)
return original_page.resolve_redirect().name
def wikimedia_article_views(site, article):
"""
Obtient le nombre de visites sur un article Wikipédia, incluant la page
canonique et toutes les pages redirigées vers celle-ci, par jour.
:param site: Objet empaquetant lAPI du site Wikipédia ciblé.
:param article: Article ciblé dans le site.
:return: Liste de couples contenant dune part un jour et dautre part
le nombre de visites associées à ce jour.
"""
# Récupération de lensemble des pages qui redirigent vers la page donnée
response = site.api('query', prop='redirects', titles=article)
page_response = list(response['query']['pages'].values())[0]
if 'missing' in page_response:
raise Exception(
'Article « {} » inexistant sur {}'
.format(article, site.host)
)
# Si la réponse ninclut pas la clé «redirects», cest quil nexiste
# aucune redirection vers la page
redirects = [
item['title'] for item in
page_response['redirects']
] if 'redirects' in page_response else []
# Somme le nombre de visites sur chacune des pages
return sum(
(wikimedia_page_views(site, page)
for page in redirects + [article]),
start=collections.Counter()
)
def wikimedia_mean_article_views(site, article):
"""
Obtient des statistiques moyennes sur les vues dun article Wikipédia par
jour de lannée (omettant le 29 février pour les années bissextiles).
:param site: Objet empaquetant lAPI du site Wikipédia ciblé.
:param article: Article ciblé dans le site.
:return: Tableau de taille 365 contenant le nombre de visites moyennes pour
chaque jour de lannée.
"""
data = wikimedia_article_views(site, article)
# Fait la moyenne pour chaque jour hormis le 29 février
accumulator = {}
datemonth_format = '%m%d'
for day in range(1, 366):
datemonth = datetime.fromordinal(day).strftime(datemonth_format)
accumulator[datemonth] = []
for date_str, views in data.items():
date = datetime.strptime(date_str, wikimedia_date_format)
if not (date.month == 2 and date.day == 29):
datemonth = date.strftime(datemonth_format)
accumulator[datemonth].append(views)
for datemonth, value in accumulator.items():
accumulator[datemonth] = sum(value) / len(value) if value else 0
# Rassemble les valeurs moyennes pour chaque jour dans l'ordre de l'année
return [item[1] for item in sorted(
list(accumulator.items()),
key=lambda x: x[0]
)]
def create_year_plot():
"""
Initialise un graphe avec en abscisse les 365 jours dune année non
bissextile.
:return: Figure et axes Matplotlib.
"""
fig = pyplot.figure(figsize=(4.7, 3.3))
ax = fig.add_subplot(111)
ax.set_xlabel('Jours de lannée')
ax.set_xticks([
datetime(1, month, 1).toordinal()
for month in range(1, 13)
])
ax.set_xticklabels(calendar.month_abbr[1:13])
return fig, ax
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
if len(sys.argv) < 4:
print("""Utilisation: {} [project] [output] [article]...
Obtient les statistiques moyenne de vue de pages wiki.
Paramètres:
project Projet Wikipédia ciblé (par exemple fr.wikipedia.org).
output Nom du fichier de sortie sera sauvé le graphe, ou '-' pour
afficher le résultat à lécran.
article Nom(s) darticle(s) Wikipédia ciblé(s).
Au moins un article doit être donné. Le nombre de visites est lissé avec un
noyau gaussien décart-type 10 jours. Les redirections darticle sont suivies
et toute visite sur une page de redirection pointant vers larticle est
dénombrée comme une visite sur la page canonique.
""".format(sys.argv[0]), end='', file=sys.stderr)
sys.exit(1)
project = sys.argv[1]
output = sys.argv[2]
articles = sys.argv[3:]
site = mwclient.Site(project)
output_to_file = output != '-'
if output_to_file:
matplotlib.use('pgf')
matplotlib.rcParams.update({
'pgf.texsystem': 'xelatex',
'font.family': 'serif',
'text.usetex': True,
'pgf.rcfonts': False,
})
fig, ax = create_year_plot()
for article in articles:
canonical_article = wikimedia_article_canonical_name(site, article)
if article != canonical_article:
logger.info(
'Suivi de la redirection de « {} » en « {} »'
.format(article, canonical_article)
)
data = wikimedia_mean_article_views(site, canonical_article)
ax.plot(year_smooth_gaussian(data, 10), label=canonical_article)
ax.set_ylabel('Vues par jour')
fig.legend(framealpha=1)
fig.autofmt_xdate()
fig.tight_layout()
if output_to_file:
pyplot.savefig(output)
else:
pyplot.show()

91
data/plot_pageviews.py Normal file
View File

@ -0,0 +1,91 @@
import calendar
import collections
from datetime import datetime
from fetch import wikipedia_pageviews, mediawiki_api
import logging
import matplotlib
from matplotlib import pyplot
import sys
# Objet pour afficher le journal dexécution
logger = logging.getLogger('pageviews')
logging.basicConfig(level=logging.INFO)
if len(sys.argv) < 4:
print("""Utilisation: {} [project] [output] [article]...
Représente sur un graphe les statistiques moyennes de vues de pages wiki.
Paramètres:
project Projet Wikipédia ciblé (par exemple fr.wikipedia.org).
output Nom du fichier de sortie sera sauvé le graphe, ou '-' pour
afficher le résultat à lécran.
article Nom(s) darticle(s) Wikipédia ciblé(s).
Au moins un article doit être donné. Le nombre de visites est lissé avec un
noyau gaussien décart-type 10 jours. Les redirections darticle sont suivies
et toute visite sur une page de redirection pointant vers larticle est
dénombrée comme une visite sur la page canonique.
""".format(sys.argv[0]), end='', file=sys.stderr)
sys.exit(1)
project = sys.argv[1]
output = sys.argv[2]
articles = sys.argv[3:]
site = mediawiki_api.instanciate(project)
output_to_file = output != '-'
if output_to_file:
matplotlib.use('pgf')
matplotlib.rcParams.update({
'pgf.texsystem': 'xelatex',
'font.family': 'serif',
'text.usetex': True,
'pgf.rcfonts': False,
})
fig = pyplot.figure(figsize=(4.7, 3.3))
ax = fig.add_subplot(111)
ax.set_xlabel('Jours de lannée')
ax.set_xticks([
datetime(1, month, 1).toordinal()
for month in range(1, 13)
])
ax.set_xticklabels(calendar.month_abbr[1:13])
for article in articles:
canonical = mediawiki_api.article_canonical(site, article)
if article != canonical:
logger.info(
'Suivi de la redirection de « {} » en « {} »'
.format(article, canonical)
)
del article
redirects = mediawiki_api.article_redirects(site, canonical)
mean_views = wikipedia_pageviews.mean(sum(
(wikipedia_pageviews.get(project, page)
for page in redirects + [canonical]),
start=collections.Counter()
))
ax.plot(
wikipedia_pageviews.smooth(mean_views, 10),
label=canonical
)
ax.set_ylabel('Vues par jour')
fig.legend(framealpha=1)
fig.autofmt_xdate()
fig.tight_layout()
if output_to_file:
pyplot.savefig(output)
else:
pyplot.show()

7419
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +0,0 @@
{
"name": "wikimedica-disease-search",
"version": "0.1.0",
"private": true,
"main": "index.js",
"devDependencies": {
"@babel/core": "^7.7.2",
"@babel/preset-env": "^7.7.1",
"@babel/preset-react": "^7.7.0",
"parcel-bundler": "^1.12.4"
},
"dependencies": {
"react": "^16.12.0",
"react-dom": "^16.12.0"
}
}