Наталья Кайда 13 декабря 2023

🐍⚙️ Python или Rust: что выбрать для анализа данных и машинного обучения

Популярность Python в анализе данных и ML уже неоспорима, однако быстрорастущая звезда Rust готова бросить ему вызов.
🐍⚙️ Python или Rust: что выбрать для анализа данных и машинного обучения

Python стал основным языком машинного обучения и анализа данных благодаря своей простоте, гибкости и огромному выбору вспомогательных библиотек. Процесс разработки на Python идет гораздо быстрее, чем на любом другом языке, и хотя Python довольно часто комбинируют с R и Julia, ни тот, ни другой язык не может полностью его заменить. Недавно у Python появился новый конкурент – Rust. Он гораздо сложнее Python, но у него есть два важных преимущества – высокая производительность, сопоставимая с C/C++, и максимально надежный механизм обеспечения безопасности. Теперь каждый ML-разработчик и аналитик данных должен решить для себя дилемму – какой из этих языков выбрать. Попробуем сравнить особенности Python, которые сделали его фактическим стандартом в AI/ML и анализе данных, с вескими преимуществами восходящей звезды Rust.

🐍 Библиотека питониста
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста»
🐍🎓 Библиотека собеса по Python
Подтянуть свои знания по Python вы можете на нашем телеграм-канале «Библиотека собеса по Python»
🧩🐍 Библиотека задач по Python
Интересные задачи по Python для практики можно найти на нашем телеграм-канале «Библиотека задач по Python»

Наследие Python

Python имеет простой и интуитивно понятный синтаксис, который иногда в шутку называют исполняемым псевдокодом. Эта простота и доступность приглянулись многим талантливым разработчикам: они начали создавать всевозможные дополнительные модули, библиотеки и фреймворки. В результате Python очень быстро обзавелся необъятной экосистемой, в которой есть инструменты для решения любых задач. Не все эти задачи решаются максимально эффективно – из-за невысокой производительности Python не подходит для разработки серьезных 3D-игр, например – но для анализа данных, машинного обучения и многих других вещей скорости языка вполне хватает. К тому же многие критически важные модули и библиотеки Python реализованы на уровне С и работают с соответствующей скоростью. Вот так просто выглядит чтение данных из CSV-файла в Python с помощью Pandas:

        import pandas as pd
data = pd.read_csv("mydataset.csv")
print(data.head())
    

Новый конкурент – Rust

Основные плюсы Rust высокая производительность, безопасность и многопоточность. Он отлично подходит для системного программирования, стремительно набирает популярность в серверной разработке и геймдеве. Rust – не самый очевидный выбор для анализа данных и машинного обучения. Однако в последние несколько лет, благодаря своим веским преимуществам, он все чаще применяется и в этих областях – хотя использовать его заметно сложнее. Чтение того же самого CSV-файла в Rust выглядит так:

        use std::error::Error;
use csv::ReaderBuilder;

fn main() -> Result<(), Box<dyn Error>> {
    let file = std::fs::File::open("mydataset.csv")?;
    let mut rdr = ReaderBuilder::new().has_headers(true).from_reader(file);

    for result in rdr.records() {
        let record = result?;
        println!("{:?}", record);
    }

    Ok(())
}
    

Python или Rust: что проще

Python известен плавной кривой обучения: с минимальными знаниями языка уже можно писать полезные скрипты, а изучение более сложных концепций отложить на потом. По этой причине Pyhton идеально подходит:

  • Специалистам, которым нужно с помощью программирования решать профессиональные задачи – научные и инженерные.
  • Новичкам, не имеющим никакого опыта в программировании.
        # Пример кода на Python
print("Hello, World!")

    

Rust, напротив, может показаться слишком сложным для начинающих: в нем есть непростые концепции, которые надо осмыслить сразу, например, системы владения и заимствования:

  • Система владения в Rust означает, что каждый объект имеет только одного владельца, который отвечает за его уничтожение. Это предотвращает многие ошибки, связанные с утечкой памяти или двойным освобождением ресурсов.
  • Система заимствования позволяет использовать один и тот же ресурс в нескольких местах кода без необходимости его копирования. Это может значительно повысить производительность, так как не тратятся ресурсы на создание лишних копий объектов.

Однако эти концепции, среди прочих уникальных особенностей Rust, и делают его таким быстрым, безопасным и надежным. Это как раз тот случай, когда усилия, затраченные на изучение языка, будут многократно компенсированы качеством и производительностью готовых приложений.

        // Пример кода на Rust
fn main() {
   println!("Hello, World!");
}

    

Rust или Python: что быстрее

Сравним производительность Rust и Python на решении одной и той же задачи. Напишем рекурсивный код, который вычисляет первые 35 чисел в последовательности Фибоначчи и выводит время, затраченное на это вычисление.

Вычисление последовательности Фибоначчи на Rust:

        use std::time::Instant;

fn fibonacci(n: u32) -> u32 {
  match n {
      0 => 0,
      1 => 1,
      _ => fibonacci(n - 1) + fibonacci(n - 2),
  }
}

fn main() {
  let start_time = Instant::now();

  for i in 0..35 {
      println!("{}", fibonacci(i));
  }

  let duration = start_time.elapsed();

  println!("Время выполнения: {:.2} секунд", duration.as_secs_f64());
}
    

Результат:

        0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
10946
17711
28657
46368
75025
121393
196418
317811
514229
832040
1346269
2178309
3524578
5702887
Время выполнения: 0.05 секунд
    

Вариант на Python:

        import time

def fibonacci(n):
   if n <= 0:
       return 0
   elif n == 1:
       return 1
   else:
       return fibonacci(n-1) + fibonacci(n-2)

start_time = time.time()

for i in range(35):
   print(fibonacci(i))

end_time = time.time()

print(f"Время выполнения : {end_time - start_time} секунд")

    

Результат:

        0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
10946
17711
28657
46368
75025
121393
196418
317811
514229
832040
1346269
2178309
3524578
5702887
Время выполнения : 7.8505754470825195 секунд


    

Очевидно, что Rust справился с задачей быстрее:).

Библиотеки и фреймворки

С экосистемой Python сложно соперничать: библиотеки для математических вычислений, анализа данных и работы с нейронными сетями (Numpy, Pandas, TensorFlow, PyTorch, Scikit-Learn и т. д.) стали стандартом де-факто в индустрии анализа данных, Data Science и ML/AI.

Однако Rust и его экосистема быстро развиваются:

  • В распоряжении разработчиков есть модуль ndarray с аналогичной NumPy функциональностью.
  • Имеется аналог Pandas для анализа данных – Polars.
  • Есть библиотека statrs для статистических расчетов и анализа.
  • Библиотека Tangram используется для машинного обучения и прогнозирования.
  • Linfa, Autograd и SmartCore предоставляют функциональность, сходную с PyTorch и TensorFlow.

Кроме того, PyTorch и TensorFlow тоже можно использовать в Rust, а список DS/ML/AI библиотек, разработанных специально для Rust, регулярно пополняется.

🤖 Библиотека Data scientist’а
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Data scientist’а»
🤖🎓 Библиотека Data Science для собеса
Подтянуть свои знания по DS вы можете на нашем телеграм-канале «Библиотека Data Science для собеса»
🤖🧩 Библиотека задач по Data Science
Интересные задачи по DS для практики можно найти на нашем телеграм-канале «Библиотека задач по Data Science»

Безопасность и конкурентость

В отличие от Python, Rust обеспечивает безопасность памяти: программа на Rust не может случайно обратиться к неверному адресу в памяти и вызвать ошибку. Это делает Rust более надежным и безопасным для использования в критических приложениях.

Кроме того, Rust является языком с конкурентной моделью выполнения: несколько операций могут выполняться параллельно без необходимости блокировки или синхронизации. Это позволяет Rust достигать более высокой производительности, чем Python, особенно при работе с большими объемами данных.

Для создания пула потоков и выполнения задач параллельно в Python можно использовать модуль concurrent.futures из стандартной библиотеки:

        import concurrent.futures

def process_data(data_chunk):
    return data_chunk * 2

data_chunks = [1, 2, 3, 4, 5]

with concurrent.futures.ThreadPoolExecutor() as executor:
    future_to_data = {executor.submit(process_data, data_chunk): data_chunk for data_chunk in data_chunks}
    for future in concurrent.futures.as_completed(future_to_data):
        data_chunk = future_to_data[future]
        try:
            data = future.result()
        except Exception as exc:
            print(f'{data_chunk} исключение: {exc}')
        else:
            print(f'{data_chunk} обработано: {data}')
    

Для параллельных операций в Rust используется библиотека Rayon:

        use rayon::prelude::*;

fn process_data(data_chunk: &mut Vec<i32>) {
   *data_chunk = data_chunk.iter().map(|&x| x * 2).collect();
}

fn main() {
   let mut data_chunks: Vec<Vec<i32>> = vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]];

   data_chunks.par_iter_mut().for_each(|chunk| {
       process_data(chunk);
   });

   println!("{:?}", data_chunks);
}

    

Управление памятью при обработке данных

В Python управление памятью абстрагировано, что делает его более простым для разработчиков и специалистов по анализу данных, поскольку они могут сосредоточиться на алгоритмах, а не на тонкостях использования памяти. Однако это может привести к проблемам при работе с большими объемами информации: Python использует автоматическую сборку мусора, которая может быть неэффективной для объемных наборов данных.

В Rust есть возможность управлять памятью на более низком уровне, обеспечивая эффективное использование ресурсов. Это позволяет точно контролировать использование памяти и может быть очень полезно при работе с большими наборами данных. В приведенном ниже примере происходит следующее:

  • large_array создается внутри функции main и становится владельцем Vec. Это означает, что large_array отвечает за освобождение памяти, выделенной для Vec, когда он выходит из области видимости.
  • sum создается как ссылка на результат вызова large_array.iter().sum(). Таким образом, sum не владеет данными, а просто ссылается на них.
  • В конце функции main, когда large_array выходит из области видимости, память, выделенная для Vec, автоматически освобождается. Это гарантируется системой управления памятью Rust, которая автоматически освобождает память, когда владелец выходит из области видимости.
        fn main() {
   let mut large_array: Vec<f64> = vec![1.0; 1_000_000];
   let sum: f64 = large_array.iter().sum();
   println!("Сумма элементов равна {}", sum);
}
    

Параллелизм и многопоточность

В Python многопоточность и параллелизм можно реализовать помощью модулей threading и multiprocessing, но они имеют свои особенности и ограничения. В частности, из-за Global Interpreter Lock (GIL) в Python, многопоточность не всегда может привести к увеличению производительности при выполнении задач, которые интенсивно нагружают процессор. В то же время модуль multiprocessing позволяет обойти GIL, создавая отдельные процессы, каждый из которых имеет свой собственный интерпретатор Python и свою собственную копию памяти. Приведенный ниже пример демонстрирует многопроцессорность, которая является формой параллелизма:

        import multiprocessing
import time

def heavy(data, i, proc):
    for index in range(len(data)):
        data[index] += 1
    print(f"Обработка № {i} ядро {proc}")

def sequential(calc, proc, data):
    print(f"Запускаем поток № {proc}")
    for i in range(calc): 
       heavy(data, i, proc)
    print(f"{calc} обработок данных завершено. Процессор № {proc}")
 
 
def processesed(procs, calc):
   # procs - количество ядер
   # calc - количество операций на ядро
 
   processes = []
   manager = multiprocessing.Manager()
   data = manager.list([num for num in range(1, 10)])
 
   # делим вычисления на количество ядер
   for proc in range(procs):
       p = multiprocessing.Process(target=sequential, args=(calc, proc, data))
       processes.append(p)
       p.start()

 # ждем, пока все ядра завершат свою работу
   for p in processes:
       p.join()

   return data


start = time.time()
# узнаем количество ядер у процессора
n_proc = multiprocessing.cpu_count()
# вычисляем, сколько циклов вычислений будет приходиться
# на 1 ядро, чтобы в сумме получилось 80+
calc = 80 // n_proc + 1
data = processesed(n_proc, calc)
end = time.time()
print(f"Количество ядер в процессоре: {n_proc}\n"
   f"На каждом ядре произведено обработок данных: {calc}\n"
   f"Итого {n_proc * calc} обработок за: {end - start}\n"
   f"Обработанные данные: {data}")

    

В Rust есть библиотека Rayon, которая позволяет легко преобразовать последовательные вычисления в параллельные. Rayon гарантирует отсутствие условий гонки данных, что идеально обеспечивает параллельность вычислений:

        use rayon::prelude::*;
use num_cpus;

fn main() {
  let mut data: Vec<i32> = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];

  // Определяем количество ядер процессора
  let num_cpus = num_cpus::get();

  // Вычисляем количество циклов вычислений для каждого ядра
  let num_cycles = 80 / num_cpus + 1;

  // Выполняем циклы вычислений
  for _ in 0..num_cycles {
      data.par_iter_mut().for_each(|x| {
          *x += 1;
      });
  }

  println!("{:?}", data);
}

    

Визуализация данных

Python располагает несколькими библиотеками для визуализации данных: Мatplotlib, Seaborn, Bokeh, Plotly, Altair и т.д. Самые популярные из них – Мatplotlib и Seaborn. Эти библиотеки позволяют легко создавать графики и диаграммы, что делает визуализацию данных в Python максимально простой и удобной:

        import matplotlib.pyplot as plt

# Данные
data = [1, 2, 3, 4, 5]

# Названия месяцев
months = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май']

# Создаем график
fig, ax = plt.subplots()

# Выводим данные
ax.plot(months, data)

# Устанавливаем названия осей
ax.set_xlabel('Месяц')
ax.set_ylabel('Данные')

plt.show()

    

Результат:

🐍⚙️ Python или Rust: что выбрать для анализа данных и машинного обучения

В Rust для визуализации можно использовать библиотеку plotters:

        use plotters::prelude::*;

fn main() -> Result<(), Box<dyn std::error::Error>> {
  let data = vec![1, 2, 3, 4, 5];
  let months = vec!["Январь", "Февраль", "Март", "Апрель", "Май"];

  let root = BitMapBackend::new("plot.png", (640, 480)).into_drawing_area();
  root.fill(&WHITE)?;

  let mut chart = ChartBuilder::on(&root)
      .caption("Месячная статистика", ("Arial", 50).into_font())
      .margin(5)
      .x_label_area_size(30)
      .y_label_area_size(30)
      .build_cartesian_2d(months.iter().cloned().map(|m| m.to_string()).collect::<Vec<String>>(), 0..10)?;

  chart.configure_mesh().draw()?;

  chart.draw_series(LineSeries::new(
      data.iter().enumerate().map(|(i, &v)| (months[i].to_string(), v)),
      &BLUE,
  ))?;

  Ok(())
}

    

Интеграция с другими языками программирования

Python может бесшовно интегрироваться с библиотеками C и C++ с помощью Cython. Это дает возможность использовать функции и данные из этих библиотек в Python-коде.

Если у вас есть библиотека my_lib на C:

        // my_lib.c
#include <stdlib.h>

int add(int a, int b) {
   return a + b;
}
    

Ее можно скомпилировать:

        gcc -shared -o my_lib.so my_lib.c
    

И вызвать в Python:

        from ctypes import CDLL

# Загрузка библиотеки
my_lib = CDLL('./my_lib.so')

# Вызов функции add
result = my_lib.add(1, 2)
print(result) # Выводит: 3
    

Rust предлагает возможности для интеграции с библиотеками C через Foreign Function Interface (FFI). FFI позволяет вызывать функции, определенные в другом языке программирования, из кода на Rust. Это достигается путем определения внешней функции в Rust с сигнатурой функции, совместимой с C, и затем динамического связывания с общей библиотекой, содержащей реализацию функции:

        // Пример использования FFI для вызова функции C из Rust
extern "C" {
   fn my_c_function(arg1: i32, arg2: f64) -> f64;
}

fn main() {
   let result = unsafe { my_c_function(42, 3.14) };
   println!("Result: {}", result);
}


    

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

        // Rust код, использующий PyO3 для создания модуля Python
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

#[pyfunction]
fn process(data: Vec<i32>) -> Vec<i32> {
   data.iter().map(|x| x * 2).collect()
}

#[pymodule]
fn rust_module(py: Python, m: &PyModule) -> PyResult<()> {
   m.add_function(wrap_pyfunction!(process, m)?)?;
   Ok(())
}
    

Скомпилированный модуль в коде Python можно использовать так:

        import rust_module

data = [1, 2, 3, 4, 5]
result = rust_module.process(data)
print(result) # Выводит: [2, 4, 6, 8, 10]
    

Подведем итоги

Выбор между Python и Rust для анализа данных и машинного обучения – сложная дилемма, поскольку оба языка предлагают уникальные преимущества:

  • Python обладает максимально простым, понятным и гибким синтаксисом, располагает обширной экосистемой библиотек и фреймворков для машинного обучения и работы с данными. Это делает его отличным выбором для разработчиков, которые только начинают изучать анализ данных и машинное обучение.
  • С другой стороны, Rust обеспечивает максимальную производительность, безопасность, эффективную поддержку многопоточности и параллелизма. Rust отлично подходит для продвинутых разработчиков, создающих ПО для работы с большими наборами данных и выполнения сложных вычислительных задач.

В конечном итоге выбор между Python и Rust зависит от ваших конкретных потребностей и уровня опыта. Если вы только начинаете изучать анализ данных и машинное обучение, то лучше выбрать Python. Если у вас уже есть опыт работы с другими языками программирования, а ваш проект нуждается в более высокой производительности и безопасности, то Rust будет самым подходящим вариантом.

***

Материалы по теме

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ