2019-11-26 05:43:40 +00:00
|
|
|
|
import collections
|
2019-12-05 02:05:18 +00:00
|
|
|
|
from datetime import datetime, timedelta
|
2019-11-26 05:43:40 +00:00
|
|
|
|
from .http import session
|
|
|
|
|
import numpy
|
|
|
|
|
from scipy import stats
|
|
|
|
|
|
|
|
|
|
# Chemin racine pour les API Wikimedia
|
2019-11-26 18:41:46 +00:00
|
|
|
|
pageviews_root = 'https://wikimedia.org/api/rest_v1'
|
2019-11-26 05:43:40 +00:00
|
|
|
|
|
2019-11-26 18:41:46 +00:00
|
|
|
|
# Patron d’accès à l’API pageviews pour un article
|
|
|
|
|
pageviews_article_endpoint = '/' + '/'.join([
|
2019-11-26 05:43:40 +00:00
|
|
|
|
'metrics', 'pageviews', 'per-article', '{project}',
|
|
|
|
|
'{access}', '{agent}', '{article}', '{granularity}',
|
|
|
|
|
'{start}', '{end}'
|
|
|
|
|
])
|
|
|
|
|
|
2019-11-26 18:41:46 +00:00
|
|
|
|
# Chemin d’accès à l’API pageviews pour un projet complet
|
|
|
|
|
pageviews_project_endpoint = '/' + '/'.join([
|
|
|
|
|
'metrics', 'pageviews', 'aggregate', '{project}',
|
|
|
|
|
'{access}', '{agent}', '{granularity}',
|
|
|
|
|
'{start}', '{end}'
|
|
|
|
|
])
|
|
|
|
|
|
2019-11-26 05:43:40 +00:00
|
|
|
|
# Format de dates utilisée pour l’API Wikimedia
|
2019-11-26 18:41:46 +00:00
|
|
|
|
pageviews_date_format = '%Y%m%d'
|
2019-11-26 05:43:40 +00:00
|
|
|
|
|
|
|
|
|
# Date de première disponibilité des pageviews sur l’API Wikimedia
|
2019-11-26 18:41:46 +00:00
|
|
|
|
pageviews_first_data = datetime(2015, 7, 1)
|
2019-11-26 05:43:40 +00:00
|
|
|
|
|
2019-12-05 02:05:18 +00:00
|
|
|
|
# Date de dernière disponibilité des pageviews sur l’API Wikimedia
|
|
|
|
|
pageviews_last_data = datetime.today() - timedelta(days=1)
|
|
|
|
|
|
2019-11-26 05:43:40 +00:00
|
|
|
|
# Tableau contenant tous les jours de l’année de 1 à 365
|
|
|
|
|
year_all_days = numpy.arange(1, 366)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _year_date_distance(a, b):
|
|
|
|
|
"""
|
|
|
|
|
Calcule la distance entre deux jours de l’année.
|
|
|
|
|
|
|
|
|
|
:param a: Première valeur (peut être un tableau numpy).
|
|
|
|
|
:param b: Seconde valeur (peut être un tableau numpy).
|
|
|
|
|
:return: Valeur de la distance.
|
2019-11-26 15:36:07 +00:00
|
|
|
|
|
|
|
|
|
>>> _year_date_distance(10, 360)
|
|
|
|
|
15
|
|
|
|
|
>>> _year_date_distance(numpy.array([10, 182, 355]), 182)
|
|
|
|
|
[172, 0, 173]
|
2019-11-26 05:43:40 +00:00
|
|
|
|
"""
|
|
|
|
|
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 l’année en
|
|
|
|
|
utilisant un noyau gaussien.
|
|
|
|
|
|
|
|
|
|
À la bordure de fin ou de début d’année, le lissage est fait avec l’autre
|
|
|
|
|
morceau de l’anné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)
|
|
|
|
|
|
|
|
|
|
|
2019-11-26 18:41:46 +00:00
|
|
|
|
def get_aggregate(project):
|
|
|
|
|
"""
|
|
|
|
|
Obtient le nombre de visites sur Wikipédia par jour.
|
|
|
|
|
|
|
|
|
|
:param project: Projet Wikipédia ciblé.
|
|
|
|
|
:return: Compteur associant chaque jour à son nombre de visites.
|
|
|
|
|
"""
|
|
|
|
|
res = session.get(pageviews_root + pageviews_project_endpoint.format(
|
|
|
|
|
project=project,
|
|
|
|
|
access='all-access',
|
|
|
|
|
agent='user',
|
|
|
|
|
granularity='daily',
|
|
|
|
|
start=pageviews_first_data.strftime(pageviews_date_format),
|
2019-12-05 02:05:18 +00:00
|
|
|
|
end=pageviews_last_data.strftime(pageviews_date_format)
|
2019-11-26 18:41:46 +00:00
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
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 get_article(project, article):
|
2019-11-26 05:43:40 +00:00
|
|
|
|
"""
|
2019-11-26 18:41:46 +00:00
|
|
|
|
Obtient le nombre de visites sur un article Wikipédia par jour.
|
2019-11-26 05:43:40 +00:00
|
|
|
|
|
|
|
|
|
:param project: Projet Wikipédia ciblé.
|
|
|
|
|
:param article: Article ciblé dans le site.
|
|
|
|
|
:return: Compteur associant chaque jour à son nombre de visites.
|
|
|
|
|
"""
|
2019-11-26 18:41:46 +00:00
|
|
|
|
res = session.get(pageviews_root + pageviews_article_endpoint.format(
|
2019-11-26 05:43:40 +00:00
|
|
|
|
project=project,
|
|
|
|
|
article=article,
|
|
|
|
|
access='all-access',
|
|
|
|
|
agent='user',
|
|
|
|
|
granularity='daily',
|
2019-11-26 18:41:46 +00:00
|
|
|
|
start=pageviews_first_data.strftime(pageviews_date_format),
|
2019-12-05 02:05:18 +00:00
|
|
|
|
end=pageviews_last_data.strftime(pageviews_date_format)
|
2019-11-26 05:43:40 +00:00
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
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 l’année à partir d’un 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 l’anné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():
|
2019-11-26 18:41:46 +00:00
|
|
|
|
date = datetime.strptime(date_str, pageviews_date_format)
|
2019-11-26 05:43:40 +00:00
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
)]
|