Наталья Кайда 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 будет самым подходящим вариантом.

***

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

МЕРОПРИЯТИЯ

Комментарии

 
 

ВАКАНСИИ

Добавить вакансию
Senior MLE (SE)
от 5000 USD до 9000 USD
Ведущий SRE инженер
Москва, по итогам собеседования
Senior DevOps Developer
Лимасол, по итогам собеседования

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

Подпишись

на push-уведомления