Skip to content

Commit c61babb

Browse files
committed
Add citation and h-index statistics
- Legacy-Id: 12869
1 parent f180147 commit c61babb

8 files changed

Lines changed: 226 additions & 14 deletions

File tree

ietf/person/models.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import debug # pyflakes:ignore
1616

17-
from ietf.person.name import name_parts, initials
17+
from ietf.person.name import name_parts, initials, plain_name
1818
from ietf.utils.mail import send_mail_preformatted
1919
from ietf.utils.storage import NoLocationMigrationFileSystemStorage
2020

@@ -47,8 +47,7 @@ def short(self):
4747
return (first and first[0]+"." or "")+(middle or "")+" "+last+(suffix and " "+suffix or "")
4848
def plain_name(self):
4949
if not hasattr(self, '_cached_plain_name'):
50-
prefix, first, middle, last, suffix = name_parts(self.name)
51-
self._cached_plain_name = u" ".join([first, last])
50+
self._cached_plain_name = plain_name(self.name)
5251
return self._cached_plain_name
5352
def ascii_name(self):
5453
if not hasattr(self, '_cached_ascii_name'):

ietf/person/name.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ def initials(name):
5050
initials = u" ".join([ n[0]+'.' for n in given.split() ])
5151
return initials
5252

53+
def plain_name(name):
54+
prefix, first, middle, last, suffix = name_parts(name)
55+
return u" ".join([first, last])
56+
5357
if __name__ == "__main__":
5458
import sys
5559
name = u" ".join(sys.argv[1:])

ietf/stats/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
urlpatterns = patterns('',
77
url("^$", ietf.stats.views.stats_index),
8-
url("^document/(?:(?P<stats_type>authors|pages|words|format|formlang|author/documents|author/affiliation|author/country|author/continent|author/citation)/)?$", ietf.stats.views.document_stats),
8+
url("^document/(?:(?P<stats_type>authors|pages|words|format|formlang|author/documents|author/affiliation|author/country|author/continent|author/citations||author/hindex)/)?$", ietf.stats.views.document_stats),
99
url("^knowncountries/$", ietf.stats.views.known_countries_list),
1010
url("^review/(?:(?P<stats_type>completion|results|states|time)/)?(?:%(acronym)s/)?$" % settings.URL_REGEXPS, ietf.stats.views.review_stats),
1111
)

ietf/stats/views.py

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
from ietf.submit.models import Submission
2424
from ietf.group.models import Role, Group
2525
from ietf.person.models import Person
26-
from ietf.name.models import ReviewRequestStateName, ReviewResultName, CountryName
26+
from ietf.name.models import ReviewRequestStateName, ReviewResultName, CountryName, DocRelationshipName
27+
from ietf.person.name import plain_name
2728
from ietf.doc.models import DocAlias, Document, State
28-
from ietf.stats.utils import get_aliased_affiliations, get_aliased_countries
29+
from ietf.stats.utils import get_aliased_affiliations, get_aliased_countries, compute_hirsch_index
2930
from ietf.ietfauth.utils import has_role
3031

3132
def stats_index(request):
@@ -103,7 +104,8 @@ def build_document_stats_url(stats_type_override=Ellipsis, get_overrides={}):
103104
("author/affiliation", "Affiliation"),
104105
("author/country", "Country"),
105106
("author/continent", "Continent"),
106-
("author/citation", "Citations"),
107+
("author/citations", "Citations"),
108+
("author/hindex", "Impact"),
107109
], lambda slug: build_document_stats_url(stats_type_override=slug))
108110

109111

@@ -346,7 +348,7 @@ def generate_canonical_names(docalias_qs):
346348

347349
person_filters &= Q(documentauthor__document__in=docs_within_time_constraint)
348350

349-
person_qs = Person.objects.filter(person_filters)
351+
person_qs = Person.objects.filter(person_filters, documentauthor__document="draft-arkko-dual-stack-extra-lite")
350352

351353
if document_type == "rfc":
352354
doc_label = "RFC"
@@ -369,6 +371,8 @@ def count_bins(bins):
369371

370372
bins = defaultdict(list)
371373

374+
person_qs = Person.objects.filter(person_filters)
375+
372376
for name, document_count in person_qs.values_list("name").annotate(Count("documentauthor")):
373377
bins[document_count].append(name)
374378

@@ -378,7 +382,7 @@ def count_bins(bins):
378382
for document_count, names in sorted(bins.iteritems(), key=lambda t: t[0]):
379383
percentage = len(names) * 100.0 / (total_persons or 1)
380384
series_data.append((document_count, percentage))
381-
table_data.append((document_count, percentage, names))
385+
table_data.append((document_count, percentage, [plain_name(n) for n in names]))
382386

383387
chart_data.append({
384388
"data": series_data,
@@ -390,6 +394,8 @@ def count_bins(bins):
390394

391395
bins = defaultdict(list)
392396

397+
person_qs = Person.objects.filter(person_filters)
398+
393399
# Since people don't write the affiliation names in the
394400
# same way, and we don't want to go back and edit them
395401
# either, we transform them here.
@@ -410,7 +416,7 @@ def count_bins(bins):
410416
percentage = len(names) * 100.0 / (total_persons or 1)
411417
if affiliation:
412418
series_data.append((affiliation, len(names)))
413-
table_data.append((affiliation, percentage, names))
419+
table_data.append((affiliation, percentage, [plain_name(n) for n in names]))
414420

415421
series_data.sort(key=lambda t: t[1], reverse=True)
416422
series_data = series_data[:30]
@@ -428,6 +434,8 @@ def count_bins(bins):
428434

429435
bins = defaultdict(list)
430436

437+
person_qs = Person.objects.filter(person_filters)
438+
431439
# Since people don't write the country names in the
432440
# same way, and we don't want to go back and edit them
433441
# either, we transform them here.
@@ -457,7 +465,7 @@ def count_bins(bins):
457465
percentage = len(names) * 100.0 / (total_persons or 1)
458466
if country:
459467
series_data.append((country, len(names)))
460-
table_data.append((country, percentage, names))
468+
table_data.append((country, percentage, [plain_name(n) for n in names]))
461469

462470
series_data.sort(key=lambda t: t[1], reverse=True)
463471
series_data = series_data[:30]
@@ -477,6 +485,8 @@ def count_bins(bins):
477485

478486
bins = defaultdict(list)
479487

488+
person_qs = Person.objects.filter(person_filters)
489+
480490
name_country_set = set((name, country)
481491
for name, country in person_qs.values_list("name", "documentauthor__country"))
482492

@@ -497,7 +507,7 @@ def count_bins(bins):
497507
percentage = len(names) * 100.0 / (total_persons or 1)
498508
if continent:
499509
series_data.append((continent, len(names)))
500-
table_data.append((continent, percentage, names))
510+
table_data.append((continent, percentage, [plain_name(n) for n in names]))
501511

502512
series_data.sort(key=lambda t: t[1], reverse=True)
503513

@@ -506,6 +516,59 @@ def count_bins(bins):
506516
"animation": False,
507517
})
508518

519+
elif stats_type == "author/citations":
520+
stats_title = "Number of citations of {}s written by author".format(doc_label)
521+
522+
bins = defaultdict(list)
523+
524+
cite_relationships = list(DocRelationshipName.objects.filter(slug__in=['refnorm', 'refinfo', 'refunk', 'refold']))
525+
person_filters &= Q(documentauthor__document__docalias__relateddocument__relationship__in=cite_relationships)
526+
527+
person_qs = Person.objects.filter(person_filters)
528+
529+
for name, citations in person_qs.values_list("name").annotate(Count("documentauthor__document__docalias__relateddocument")):
530+
bins[citations].append(name)
531+
532+
total_persons = count_bins(bins)
533+
534+
series_data = []
535+
for citations, names in sorted(bins.iteritems(), key=lambda t: t[0], reverse=True):
536+
percentage = len(names) * 100.0 / (total_persons or 1)
537+
series_data.append((citations, percentage))
538+
table_data.append((citations, percentage, [plain_name(n) for n in names]))
539+
540+
chart_data.append({
541+
"data": sorted(series_data, key=lambda t: t[0]),
542+
"animation": False,
543+
})
544+
545+
elif stats_type == "author/hindex":
546+
stats_title = "h-index for {}s written by author".format(doc_label)
547+
548+
bins = defaultdict(list)
549+
550+
cite_relationships = list(DocRelationshipName.objects.filter(slug__in=['refnorm', 'refinfo', 'refunk', 'refold']))
551+
person_filters &= Q(documentauthor__document__docalias__relateddocument__relationship__in=cite_relationships)
552+
553+
person_qs = Person.objects.filter(person_filters)
554+
555+
values = person_qs.values_list("name", "documentauthor__document").annotate(Count("documentauthor__document__docalias__relateddocument"))
556+
for name, ts in itertools.groupby(values.order_by("name"), key=lambda t: t[0]):
557+
h_index = compute_hirsch_index([citations for _, document, citations in ts])
558+
bins[h_index].append(name)
559+
560+
total_persons = count_bins(bins)
561+
562+
series_data = []
563+
for citations, names in sorted(bins.iteritems(), key=lambda t: t[0], reverse=True):
564+
percentage = len(names) * 100.0 / (total_persons or 1)
565+
series_data.append((citations, percentage))
566+
table_data.append((citations, percentage, [plain_name(n) for n in names]))
567+
568+
chart_data.append({
569+
"data": sorted(series_data, key=lambda t: t[0]),
570+
"animation": False,
571+
})
509572

510573
return render(request, "stats/document_stats.html", {
511574
"chart_data": mark_safe(json.dumps(chart_data)),
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<h3>{{ stats_title }}</h3>
2+
3+
<div id="chart"></div>
4+
5+
<script>
6+
var chartConf = {
7+
chart: {
8+
type: 'area'
9+
},
10+
title: {
11+
text: '{{ stats_title|escapejs }}'
12+
},
13+
xAxis: {
14+
title: {
15+
text: 'Number of citations of {{ doc_label }}s by author'
16+
},
17+
max: 500
18+
},
19+
yAxis: {
20+
title: {
21+
text: 'Percentage of authors'
22+
},
23+
labels: {
24+
formatter: function () {
25+
return this.value + '%';
26+
}
27+
}
28+
},
29+
tooltip: {
30+
formatter: function () {
31+
var s = '<b>' + this.x + ' ' + (this.x == 1 ? "citation" : 'citations') + '</b>';
32+
33+
$.each(this.points, function () {
34+
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y.toFixed(1) + '%';
35+
});
36+
37+
return s;
38+
},
39+
shared: true
40+
},
41+
series: {{ chart_data }}
42+
};
43+
</script>
44+
45+
<h3>Data</h3>
46+
47+
<table class="table table-condensed stats-data">
48+
<thead>
49+
<tr>
50+
<th>Citations</th>
51+
<th>Percentage of authors</th>
52+
<th>Authors</th>
53+
</tr>
54+
</thead>
55+
<tbody>
56+
{% for citations, percentage, names in table_data %}
57+
<tr>
58+
<td>{{ citations }}</td>
59+
<td>{{ percentage|floatformat:2 }}%</td>
60+
<td>{% include "stats/includes/number_with_details_cell.html" with content_limit=10 %}</td>
61+
</tr>
62+
{% endfor %}
63+
</tbody>
64+
</table>
65+
66+
<p>Note that the citation counts do not exclude self-references.</p>

ietf/templates/stats/document_stats_author_documents.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ <h3>Data</h3>
5858
<tr>
5959
<td>{{ document_count }}</td>
6060
<td>{{ percentage|floatformat:2 }}%</td>
61-
<td>{% include "stats/includes/number_with_details_cell.html" %}</td>
61+
<td>{% include "stats/includes/number_with_details_cell.html" with content_limit=10 %}</td>
6262
</tr>
6363
{% endfor %}
6464
</tbody>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<h3>{{ stats_title }}</h3>
2+
3+
<div id="chart"></div>
4+
5+
<script>
6+
var chartConf = {
7+
chart: {
8+
type: 'column'
9+
},
10+
title: {
11+
text: '{{ stats_title|escapejs }}'
12+
},
13+
xAxis: {
14+
tickInterval: 1,
15+
title: {
16+
text: 'h-index of {{ doc_label }}s by author'
17+
}
18+
},
19+
yAxis: {
20+
title: {
21+
text: 'Percentage of authors'
22+
},
23+
labels: {
24+
formatter: function () {
25+
return this.value + '%';
26+
}
27+
}
28+
},
29+
tooltip: {
30+
formatter: function () {
31+
var s = '<b>' + ' h-index ' + this.x + '</b>';
32+
33+
$.each(this.points, function () {
34+
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y.toFixed(1) + '%';
35+
});
36+
37+
return s;
38+
},
39+
shared: true
40+
},
41+
series: {{ chart_data }}
42+
};
43+
</script>
44+
45+
<h3>Data</h3>
46+
47+
<table class="table table-condensed stats-data">
48+
<thead>
49+
<tr>
50+
<th>h-index</th>
51+
<th>Percentage of authors</th>
52+
<th>Authors</th>
53+
</tr>
54+
</thead>
55+
<tbody>
56+
{% for h_index, percentage, names in table_data %}
57+
<tr>
58+
<td>{{ h_index }}</td>
59+
<td>{{ percentage|floatformat:2 }}%</td>
60+
<td>{% include "stats/includes/number_with_details_cell.html" with content_limit=25 %}</td>
61+
</tr>
62+
{% endfor %}
63+
</tbody>
64+
</table>
65+
66+
<p>Hirsch index or h-index is a
67+
<a href="https://www.wikipedia.org/wiki/H-index">measure of the
68+
productivity and impact of the publications of an author</a>. An
69+
author with an h-index of 5 has had 5 publications each cited at
70+
least 5 times - to increase the index to 6, the 5 publications plus
71+
1 more would have to have been cited at least 6 times, each. Thus a
72+
high h-index requires many highly-cited publications.</p>
73+
74+
<p>Note that the h-index calculations do not exclude self-references.</p>
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
<a class="popover-details" href="" data-elements="{% for n in names|slice:":20" %}{{ n }}{% if not forloop.last %}|{% endif %}{% endfor %}" data-sliced="{% if names|length > 20 %}1{% endif %}">{{ names|length }}</a>
1+
{% if content_limit and names|length <= content_limit %}
2+
{% for n in names %}
3+
{{ n }}<br>
4+
{% endfor %}
5+
{% else %}
6+
<a class="popover-details" href="" data-elements="{% for n in names|slice:":20" %}{{ n }}{% if not forloop.last %}|{% endif %}{% endfor %}" data-sliced="{% if names|length > 20 %}1{% endif %}">{{ names|length }}</a>
7+
{% endif %}

0 commit comments

Comments
 (0)