Как стать автором
Обновить
109.09
Рейтинг
SkillFactory
Школа Computer Science. Скидка 10% по коду HABR

Data Science на JavaScript без Python

Блог компании SkillFactory JavaScript *Программирование *Node.JS *
Перевод
Tutorial
Автор оригинала: Cristiano L. Fontana

Мы уже писали о том, как запустить Python в браузере, а сегодня к старту флагманского курса по Data Science расскажем, как привычные для Python задачи решаются на JavaScript. Если вы знакомы только с JS и хотите попробовать Data Science, не покидая зону комфорта, (или, наоборот, хотите познакомиться с JS), просто хочется необычных экспериментов или нужно интегрировать небольшую управляемую визуализацию о статистике на сайт, читайте подробности под катом.

Автор, переводом статьи которого о множестве Мандельброта мы уже делились в блоге, также включил ссылку на репозиторий polyglot_fit с решением задачи из сегодняшней статьи на других языках.


Начальные сведения о JavaScript

Язык программирования JavaScript (далее — JS) считается всеобщим языком сети Интернет, так как его поддерживают все основные веб-браузеры. Другие языки, запускаемые в браузерах, компилируются (или транслируются) в код на JavaScript. Иногда в тонкостях программирования на JS разобраться бывает довольно сложно, но мне этот язык всё равно нравится: преимуществ у него больше, чем недостатков. Язык JavaScript создавался для работы в браузере, но его можно использовать и в других целях, например в качестве встроенного языка или для обеспечения работы серверных приложений.

Я расскажу, как написать программу, которая будет выполняться на платформе Node.js — среде выполнения, предназначенной для запуска приложений JavaScript. В Node.js мне больше всего нравится событийно-ориентированная архитектура, обеспечивающая асинхронное программирование. При таком подходе определённые функции (их обычно называют функциями обратного вызова) можно привязать к определённым событиям и запускать только после наступления привязанного к функции события. Таким образом, разработчику ПО нет необходимости создавать главный цикл приложения: об этом позаботится само вычислительное окружение.

В JavaScript также реализованы новые асинхронные функции с другим синтаксисом, но принципы событийно-ориентированной архитектуры, хотя и присутствующие в них, не так очевидны, чтобы использовать их в качестве иллюстрации темы настоящей статьи. Поэтому здесь я обойдусь традиционным подходом, задействующим функции обратного вызова, хотя в данном случае это вовсе не обязательно.

Задача программы

Задачи, решаемые в статье:

  • Считать определённое количество данных из файла CSV, содержащего наборы числовых данных, называемые квартетом Энскомба.

  • Линейно интерполировать данные (т. е. представить их в виде формулы f(x) = m·x + q).

  • Вывести результат как изображение.

Чтобы получить более полное представление об этой задаче, рекомендую прочитать предыдущие статьи из этой серии, в которых рассматривается решение аналогичной задачи с применением Python и GNU Octave и C и C++. Полный исходный код для всех примеров приведён в моём репозитории polyglot_fit на GitLab.

Установка

Перед запуском примера необходимо установить платформу Node.js вместе с менеджером пакетов npm. Чтобы установить их на Fedora, запустите следующую команду:

sudo dnf install nodejs npm

На Ubuntu:

sudo apt install nodejs npm

Затем для установки необходимых пакетов используйте команду npm. Пакеты устанавливаются в локальный подкаталог node_modules, чтобы Node.js могла найти их. Вот они:

  • CSV Parse — для синтаксического разбора файла CSV;

  • Simple Statistics — для расчёта коэффициента корреляции данных;

  • Regression-js — для определения точек, через которые будет проходить прямая линия;

  • D3-Node — для построения изображений на серверной стороне. 

Эта команда npm загрузит пакеты:

npm install csv-parse simple-statistics regression d3-node

Так ставятся комментарии:

// однострочный комментарий
/* многострочный 
комментарий */

Загрузка модулей

Загрузить модули можно с помощью метода require(), он возвращает объект, содержащий функции модуля:

const EventEmitter = require('events');
const fs = require('fs');
const csv = require('csv-parser');
const regression = require('regression');
const ss = require('simple-statistics');
const D3Node = require('d3-node');

Некоторые модули входят в стандартную библиотеку Node.js, устанавливать их не нужно.

Определение переменных

До использования переменных объявлять их как var, let или const необязательно. Однако, если тип не объявить, система определит их как глобальные. В общем случае использование глобальных переменных не приветствуется: если это не обдумать, то неминуемо возникнут программные ошибки. Переменные могут содержать данные любых типов (даже функции!). Некоторые объекты можно создавать, применив к функции конструктора оператор new:

const inputFileName = "anscombe.csv";
const delimiter = "\t";
const skipHeader = 3;
const columnX = String(0);
const columnY = String(1);

const d3n = new D3Node();
const d3 = d3n.d3;

var data = [];

Данные, считанные из файла CSV, сохраняются в массиве. Массивы в JS динамические, то есть заранее определять их размер не нужно.

Определение функций

В языке JavaScript определять функции можно несколькими способами. Например, с помощью оператора объявления функции:

function triplify(x) {
    return 3 * x;
}

// The function call is:
triplify(3);

Или через выражение, то есть присвоив функцию переменной:

var triplify = function (x) {
    return 3 * x;
}

// The function call is still:
triplify(3);

И, наконец, можно использовать стрелочную функцию-выражение — синтаксически усечённую версию функции-выражения, работающую с определёнными ограничениями. Обычно такие стрелочные функции (или, как их ещё называют, функции-стрелки) используются для записи простых однострочных действий, выполняющих элементарные вычисления над своими аргументами:

var triplify = (x) => 3 * x;

// The function call is still:
triplify(3);

Печать вывода

Вывод на терминал обычно осуществляется через встроенный в стандартную библиотеку Node.js объект console. Метод log() запускает вывод (добавляет новую строку после завершения строки):

console.log("#### Anscombe's first set with JavaScript in Node.js ####");

Объект console — более мощное средство, чем обычная функция вывода на печать; например, с его помощью можно также выводить на печать предупреждения и сообщения об ошибках. Если нужно вывести значение переменной, оно преобразуется в строку и используется объект console.log():

console.log("Slope: " + slope.toString());

Считывание данных

В Node.js для ввода/вывода используется весьма интересный подход, причём такой подход может быть как синхронным, так и асинхронным. В первом случае используются блокирующие вызовы функций, а во втором — неблокирующие. При использовании блокирующей функции программа останавливается и ждёт момента, когда функция завершит свою задачу. Неблокирующие же функции не останавливают выполнение программы, а находят способы продолжить его вплоть до завершения собственной задачи.

Узнать, завершилось ли выполнение функции, можно, периодически проверяя её состояние. Иногда функция может сама уведомлять о собственном завершении. В данной статье используется второй подход: задействуется так называемый генератор событий EventEmitter, генерирующий событие, связанное с функцией обратного вызова. При наступлении события запускается функция обратного вызова.

Вначале создайте генератор событий EventEmitter:

const myEmitter = new EventEmitter();

Затем сопоставьте состояние завершения чтения файла с событием myEmitter. Для такого простого примера, правда, вовсе не требуется идти именно этим путём, а можно использовать простой блокирующий вызов — очень мощный метод, который может оказаться весьма полезным в других ситуациях. Перед этим нужно добавить в этот раздел ещё один фрагмент, задействующий для чтения данных библиотеку CSV Parse. Эта библиотека может применять несколько способов чтения данных (конкретный способ вы можете выбрать сами). В этом примере были использованы средства стандартной библиотеки обработки последовательностей данных (stream API) с методом pipe. Для правильной работы библиотеки ей нужно указать ряд параметров, определяемых объектом:

const csvOptions = {'separator': delimiter,
                    'skipLines': skipHeader,
                    'headers': false};

Итак, вы определили параметры и теперь можете прочитать файл:

fs.createReadStream(inputFileName)
  .pipe(csv(csvOptions))
  .on('data', (datum) => data.push({'x': Number(datum[columnX]), 'y': Number(datum[columnY])}))
  .on('end', () => myEmitter.emit('reading-end'));

Ниже привожу мои комментарии к строкам этого короткого, но насыщенного фрагмента кода:

  • fs.createReadStream(inputFileName) открывает поток данных, считываемых из файла. Поток постепенно считывает файл по фрагментам.

  • .pipe(csv(csvOptions)) перенаправляет поток в библиотеку CSV Parse, выполняющую нелёгкую задачу чтения и синтаксического разбора файла.

  • .on('data', (datum) => data.push({'x': Number(datum[columnX]), 'y': Number(datum[columnY])}))

 Поясню, что означает каждая часть строки:

  • (datum) => ... определяет функцию, в которую будет передаваться каждая строка файла CSV;

  • data.push(... добавляет только что считанные данные к массиву данных;

  • {'x': ..., 'y': ...} создаёт новую точку данных с членами x и y;

  • Number(datum[columnX]) преобразует элемент в столбце X (columnX) в число;

  • .on('end', () => myEmitter.emit('reading-end')); использует созданный вами генератор событий myEmitter для уведомления о завершении чтения файла.

После того как myEmitter сгенерирует событие завершения считывания, вы будете знать, что синтаксический разбор файла полностью завершён и что его содержимое передано в массив данных.

Анализ данных

Теперь, когда массив данных заполнен, можно приступать к анализу его данных. Функция, запускающая анализ, связана с событием окончания чтения, сгенерированного определённым вами генератором событий, поэтому она никогда не запустится раньше времени. Генератор событий связывает функцию обратного вызова с этим событием и при наступлении события запускает её.

myEmitter.on('reading-end', function () {
    const fit_data = data.map((datum) => [datum.x, datum.y]);

    const result = regression.linear(fit_data);
    const slope = result.equation[0];
    const intercept = result.equation[1];

    console.log("Slope: " + slope.toString());
    console.log("Intercept: " + intercept.toString());

    const x = data.map((datum) => datum.x);
    const y = data.map((datum) => datum.y);

    const r_value = ss.sampleCorrelation(x, y);

    console.log("Correlation coefficient: " + r_value.toString());

    myEmitter.emit('analysis-end', data, slope, intercept);
});

Библиотеки статистических вычислений могут работать с данными в различных форматах, поэтому к массиву данных необходимо применить метод map(). Этот метод создаёт новый массив из существующего и применяет функцию к каждому его элементу. В этом примере имеет смысл использовать лаконичные стрелочные функции. После завершения анализа можно дождаться нового события и возобновить цикл в новой функции обратного вызова. Данная функция также способна непосредственно выводить данные на график, но в этом примере я решил поручить задачу вывода другой функции, так как процесс анализа может оказаться очень длительным. При генерировании события analysis-end соответствующие данные из этой функции также передаются в следующую функцию обратного вызова.

Вывод изображения

D3.js — чрезвычайно мощная библиотека функций графического отображения данных. С функциями библиотеки работать довольно сложно, и, возможно, кому-то они покажутся трудными для понимания, но это лучший вариант с открытым исходным кодом, который мне удалось найти для вывода графики на серверной части. Больше всего в библиотеке D3.js мне импонирует то, что она может работать с изображениями SVG. D3.js была разработана для запуска в веб-браузере, поэтому предполагается, что она должна работать с определённой веб-страницей. Работа на серверной части выполняется совсем в другой среде, поэтому работать придётся с виртуальной веб-страницей. К счастью, к нашим услугам пакет D3-Node, предельно упрощающий весь процесс.

Начните с определения некоторых важных количественных параметров, которые потребуются в дальнейшем:

const figDPI = 100;
const figWidth = 7 * figDPI;
const figHeight = figWidth / 16 * 9;
const margins = {top: 20, right: 20, bottom: 50, left: 50};

let plotWidth = figWidth - margins.left - margins.right;
let plotHeight = figHeight - margins.top - margins.bottom;

let minX = d3.min(data, (datum) => datum.x);
let maxX = d3.max(data, (datum) => datum.x);
let minY = d3.min(data, (datum) => datum.y);
let maxY = d3.max(data, (datum) => datum.y);

Вам предстоит преобразовать координаты данных в координаты графика (изображения). Для такого преобразования можно использовать шкалы: область шкалы — это пространство данных, в котором выбираются точки данных, а диапазон шкалы — это пространство изображения, где помещаются точки:

let scaleX = d3.scaleLinear()
               .range([0, plotWidth])
               .domain([minX - 1, maxX + 1]);
let scaleY = d3.scaleLinear()
               .range([plotHeight, 0])
               .domain([minY - 1, maxY + 1]);

const axisX = d3.axisBottom(scaleX).ticks(10);
const axisY = d3.axisLeft(scaleY).ticks(10);

Обратите внимание, что диапазон шкалы y инвертирован, так как в стандарте SVG начало шкалы y располагается сверху. После определения шкал начинайте рисовать график на только что созданном изображении SVG:

let svg = d3n.createSVG(figWidth, figHeight)

svg.attr('background-color', 'white');

svg.append("rect")
   .attr("width", figWidth)
   .attr("height", figHeight)
   .attr("fill", 'white');

Вначале нарисуйте интерполирующую линию, добавив к изображению SVG элемент line:

svg.append("g")
   .attr('transform', `translate(${margins.left}, ${margins.top})`)
   .append("line")
   .attr("x1", scaleX(minX - 1))
   .attr("y1", scaleY((minX - 1) * slope + intercept))
   .attr("x2", scaleX(maxX + 1))
   .attr("y2", scaleY((maxX + 1) * slope + intercept))
   .attr("stroke", "#1f77b4");

Затем для всех точек данных в нужных местах добавьте элемент circle. Главной особенностью библиотеки D3.js является то, что она связывает данные с элементами SVG, для этого воспользуйтесь методом data(). Метод enter() сообщает библиотеке, какие именно действия выполнять с новыми связанными данными:

svg.append("g")
   .attr('transform', `translate(${margins.left}, ${margins.top})`)
   .selectAll("circle")
   .data(data)
   .enter()
   .append("circle")
   .classed("circle", true)
   .attr("cx", (d) => scaleX(d.x))
   .attr("cy", (d) => scaleY(d.y))
   .attr("r", 3)
   .attr("fill", "#ff7f0e");

Последние выводимые на изображение элементы — это оси с соответствующими метками; они выводятся как наложение на линии и окружности графика:

svg.append("g")
   .attr('transform', `translate(${margins.left}, ${margins.top + plotHeight})`)
   .call(axisX);

svg.append("g")
   .append("text")
   .attr("transform", `translate(${margins.left + 0.5 * plotWidth}, ${margins.top + plotHeight + 0.7 * margins.bottom})`)
  .style("text-anchor", "middle")
  .text("X");

svg.append("g")
   .attr('transform', `translate(${margins.left}, ${margins.top})`)
   .call(axisY);

svg.append("g")
   .attr("transform", `translate(${0.5 * margins.left}, ${margins.top + 0.5 * plotHeight})`)
   .append("text")
   .attr("transform", "rotate(-90)")
  .style("text-anchor", "middle")
  .text("Y");

И последнее действие — сохранение графика в файл SVG. Я выбрал вариант синхронной записи файла, чтобы продемонстрировать работу второго подхода:

fs.writeFileSync("fit_node.svg", d3n.svgString());

Результаты

Запуск скрипта предельно прост:

node fitting_node.js

Результат его выполнения:

#### Anscombe's first set with JavaScript in Node.js ####
Slope: 0.5
Intercept: 3
Correlation coefficient: 0.8164205163448399

Вот результирующее изображение, созданное мной с помощью библиотек D3.js и Node.js:

Заключение

Конечно же, есть варианты: Python можно запускать прямо из Node.JS, или же обращаться к объектам JS в коде Python через PyNode. Подобно этому специалист из любой области может стать дата-сайентистом: первая специализация поможет видеть данные в своей области знаний изнутри, понимать скрытые причины появления данных и то, как извлечь из них пользу.

Если вам интересно работать с данными, вы можете присмотреться к программе нашего курса по Data Science или прочитать о том, как правильно подойти к изучению науки о данных, а если для вас привлекательнее JS, то вы можете обратить внимание на курс по Frontend-разработке; также можно узнать, как освоить другие специальности с нуля или прокачаться:

Data Science и Machine Learning

Python, веб-разработка

Мобильная разработка

Java и C#

От основ — в глубину

А также:

Теги: skillfactoryjsdata sciencemachine learningcsvsvgпрограммированиеnode.jsnodejsинтерполяция
Хабы: Блог компании SkillFactory JavaScript Программирование Node.JS
Всего голосов 7: ↑4 и ↓3 +1
Комментарии 1
Комментарии Комментарии 1

Похожие публикации

Лучшие публикации за сутки

Информация

Дата основания
Местоположение
Россия
Сайт
www.skillfactory.ru
Численность
201–500 человек
Дата регистрации
Представитель
Skillfactory School

Блог на Хабре