Aspiring Data Science
368 subscribers
417 photos
11 videos
10 files
1.84K links
Заметки экономиста о программировании, прогнозировании и принятии решений, научном методе познания.
Контакт: @fingoldo

I call myself a data scientist because I know just enough math, economics & programming to be dangerous.
Download Telegram
#python #codegems

Встроенная функция property часто используется как декоратор, но в действительности она является классом. В Python функции и классы нередко взаимозаменяемы, поскольку являются вызываемыми объектами и не существует оператора new для создания объекта, поэтому вызов конструктора ничем не отличается от вызова фабричной функции. Вот полная сигнатура конструктора класса property: property(fget=None, fset=None, fdel=None, doc=None).

Функция vars не умеет работать с классами, в которых определен атрибут slots и нет атрибута dict (в отличие от функции dir, которая справляется с такими экземплярами).
Без аргумента vars() делает то же самое, что locals(): возвращает словарь, описывающий локальную область видимости.

Метод getattr всегда вызывается после getattribute и только в том случае, когда getattribute возбуждает исключение AttributeError. Чтобы при
получении атрибутов obj не возникало бесконечной рекурсии, в реализации getattribute следует использовать super().getattribute(obj, name).

Встроенный тип type на самом деле является метаклассом – классом по умолчанию для определенных пользователем классов.

В способе обработки атрибутов в Python существует важная асимметрия. При чтении атрибута через экземпляр обычно возвращается атрибут, определенный в этом экземпляре, а если такого атрибута в экземпляре не существует, то атрибут класса. С другой стороны, в случае присваивания атрибуту экземпляра обычно создается атрибут в этом экземпляре, а класс вообще никак не затрагивается. Эта асимметрия распространяется и на дескрипторы, в результате чего образуются две категории дескрипторов, различающиеся наличием или отсутствием метода set. Если set присутствует, то класс является переопределяющим дескриптором, иначе непереопределяющим.

Требование явно объявлять self первым аргументом методов – одно из противоречивых проектных решений в Python.Простота – даже элегантность – реализации достигается за счет пользовательского интерфейса: сигнатура метода – def zfill(self, width) – визуально не соответствует его вызову – label.zfill(8).

Метапрограммирование классов – это искусство создания или настройки классов во время выполнения. Классы в Python – полноправные объекты, поэтому функция может в любой момент создать новый класс, не используя ключевое слово class. Декораторы классов – также функции, которые дополнительно умеют инспектировать и изменять декорированный класс и даже заменять его другим. Наконец, метаклассы – самое продвинутое средство метапрограммирования классов: они позволяют создавать целые категории классов со специальными характеристиками.

Обычно мы воспринимаем type как функцию, которая возвращает класс объекта, потому что именно это делает выражение type(my_object): возвращает my_object.class.
Однако type – это класс, который создает новый класс, если вызывается с тремя аргументами. Рассмотрим следующий простой класс:
class MyClass(MySuperClass, MyMixin):
x = 42
def x2(self):
return self.x * 2

С помощью конструктора type мы можем создать MyClass во время выполнения:
MyClass = type('MyClass',
(MySuperClass, MyMixin),
{'x': 42, 'x2': lambda self: self.x * 2},
)

Этот вызов type функционально эквивалентен предыдущему предложению
блока class MyClass.
1🔥1
#python #codegems

Дескрипторы – это способ повторного использования одной и той же логики доступа в нескольких атрибутах. Например, типы полей в объектно-ориентированных отображениях вроде Django ORM и SQL Alchemy – дескрипторы, управляющие потоком данных от полей в записи базы данных к атрибутам Python-объекта и обратно. Дескриптор – это класс, который реализует динамический протокол, содержащий методы get, set и delete. Класс property реализует весь протокол дескриптора.

Пример использования дескриптора:
class Quantity:
def __set_name__(self, owner, name):
self.storage_name = name
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.storage_name] = value
else:
msg = f'{self.storage_name} must be > 0'
raise ValueError(msg)
# __get__ не нужен
class LineItem:
weight = Quantity()
price = Quantity()
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price


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

Воображаемый магазин натуральных пищевых продуктов столкнулся с неожиданной проблемой: каким-то образом была создана строка заказа с пустым описанием, и теперь заказ невозможно выполнить. Чтобы предотвратить такие инциденты в будущем, мы создадим новый дескриптор, NonBlank. Проектируя NonBlank, мы обнаруживаем, что он очень похож на дескриптор Quantity, а отличается только логика проверки. Это наводит на мысль о рефакторинге и заведении двух базовых классов: завести абстрактный класс Validated, переопределяющий метод set, вызывая метод validate, который должен быть реализован в подклассах. Затем мы переписываем Quantity и реализуем NonBlank, наследуя классу Validated, так что остается лишь написать методы validate. Соотношение между классами Validated, Quantity и NonBlank – пример паттерна проектирования Шаблонный метод, который в классической книге «Паттерны проектирования» описывается следующим образом: Шаблонный метод определяет алгоритм в терминах абстрактных операций, которые переопределяются в подклассах для обеспечения конкретного поведения.

import abc
class Validated(abc.ABC):
def __set_name__(self, owner, name):
self.storage_name = name
def __set__(self, instance, value):
value = self.validate(self.storage_name, value)
instance.__dict__[self.storage_name] = value
@abc.abstractmethod
def validate(self, name, value):
"""вернуть проверенное значение или возбудить ValueError"""

class Quantity(Validated):
"""число, большее нуля"""
def validate(self, name, value):
if value <= 0:
raise ValueError(f'{name} must be > 0')
return value
class NonBlank(Validated):
"""строка, содержащая хотя бы один символ, отличный от пробела"""
def validate(self, name, value):
value = value.strip()
if not value:
raise ValueError(f'{name} cannot be blank')
return value

import model_v5 as model
class LineItem:
description = model.NonBlank()
weight = model.Quantity()
price = model.Quantity()
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
#python #books

Ссылки на посты по книжке "Л. Рамальо. Python – к вершинам мастерства: Лаконичное и эффективное программирование" (в оригинале - Fluent Python, 2nd Edition). Содержат материал, который показался мне интересным и вошёл в категорию #codegems.

Затрагиваются механизмы сопоставления (match), классы данных и их аналоги, аннотирование типами, инструменты itertools, работа с классами/ООП, генераторы, контекстные менеджеры, асинхронка, дескрипторы классов.

Пробегитесь по темам, если есть незнакомые слова, возможно, есть смысл перечитать актуальную доку Питон )

1. [], {} и ()/match/ChainMap/MappingProxyType
2. class init/dict/json
3. unicode: NFC/NDF, strxfrm/NamedTuple/dataclass
4. more dataclass/typehints
5. weakrefs/functional programming/more typehints
6. Any/|/TypeVar/TypeAlias/typing.Protocol
7. positional-only/closures/singledispath/decorator via class
8. getattr/reduce via initializer/zip,zip_longest/principle of failing fast
9. goose typing/vurtual subclass/Hashable/ABC/Decimal
10. UserDict, UserList, UserString/MRO/mixin/get_annotations
11. (sub)generator/coprogram/type: ignore/with/@contextmanager
12. else in for,while,try/scientific sins/GIL/getswitchinterval/asyncio
13. asyncio.to_thread/asyncpg/asyncio.Semaphore/async with/keyword.iskeyword
14. property/vars/metaprogramming
15. class descriptors

Если решите читать книгу - ТОЛЬКО в оригинале, русский перевод плох.
1
#sklearn #codegems

Прошло больше года, и только сейчас в сайкит-лёрн замёрджили ГОТОВУЮ ветку, которая втрое ускоряет расчёт classification_report.

Adrin Jalali закодил решение в течение суток с момента, как я нашёл дублирующие вызовы в sklearn. Остальное время заняли бюрократические согласования.

Вывод: open-source это страшная бюрократия, не надейтесь, что вам что-то быстро исправят, делайте сами. Вместе с тем, не ленитесь создавать хотя бы багрепорты, чтобы следующие поколения DS-ов хоть немного могли почиллить 😅

Примерно с той же эффективностью можно оставлять багрепорты или запросы функциональности для catboost - да, сделают в течение года-полутора (что по сути вечность), но я таким путём за последние N лет пропихнул улучшений 4-5 туда.
🔥3
#numpy #codegems #vectorize

Будьте осторожны с vectorize )
💯1
#gpt #llms #codegems #openai

Красивый способ извлечь текстовые данные в структурированном виде. Пример Extracting data from research papers using Structured Outputs.

from pydantic import BaseModel
from openai import OpenAI

client = OpenAI()

class ResearchPaperExtraction(BaseModel):
title: str
authors: list[str]
abstract: str
keywords: list[str]

completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "You are an expert at structured data extraction. You will be given unstructured text from a research paper and should convert it into the given structure."},
{"role": "user", "content": "..."}
],
response_format=ResearchPaperExtraction,
)

research_paper = completion.choices[0].message.parsed


Example response:

{
"title": "Application of Quantum Algorithms in Interstellar Navigation: A New Frontier",
"authors": [
"Dr. Stella Voyager",
"Dr. Nova Star",
"Dr. Lyra Hunter"
],
"abstract": "This paper investigates the utilization of quantum algorithms to improve interstellar navigation systems. By leveraging quantum superposition and entanglement, our proposed navigation system can calculate optimal travel paths through space-time anomalies more efficiently than classical methods. Experimental simulations suggest a significant reduction in travel time and fuel consumption for interstellar missions.",
"keywords": [
"Quantum algorithms",
"interstellar navigation",
"space-time anomalies",
"quantum superposition",
"quantum entanglement",
"space travel"
]
}
#codegems #seedir

Классная утилита seedir:

import seedir as sd

sd.seedir(
DATA_FOLDER,
style="lines",
itemlimit=10,
depthlimit=3,
exclude_folders=".ipynb_checkpoints",
sort=True,
)


data/
├─categories/
│ └─categories/
│ ├─unique.item_brand.parquet
│ ├─unique.item_category.parquet
│ ├─unique.item_id.parquet
│ ├─unique.item_shop.parquet
│ ├─unique.user_age.parquet
│ ├─unique.user_brands.parquet
│ ├─unique.user_categories.parquet
│ ├─unique.user_consumption_2.parquet
│ ├─unique.user_gender.parquet
│ └─unique.user_geography.parquet
├─processed/
│ ├─train/
│ │ ├─_file_list.txt
│ │ ├─_metadata
│ │ ├─_metadata.json
│ │ ├─part_0.parquet
│ │ └─schema.pbtxt
│ └─valid/
│ ├─_file_list.txt
│ ├─_metadata
│ ├─_metadata.json
│ ├─part_0.parquet
│ └─schema.pbtxt
├─train/
│ └─part.0.parquet
├─valid/
│ └─part.0.parquet
└─workflow/
├─categories/
│ ├─unique.item_brand.parquet
│ ├─unique.item_category.parquet
│ ├─unique.item_id.parquet
│ ├─unique.item_shop.parquet
│ ├─unique.user_age.parquet
│ ├─unique.user_brands.parquet
│ ├─unique.user_categories.parquet
│ ├─unique.user_consumption_2.parquet
│ ├─unique.user_gender.parquet
│ └─unique.user_geography.parquet
├─metadata.json
└─workflow.pkl
#codegems #frameworks

С заменой sklearn/pytorch я не согласен, это глупость. selectolax я не знаю, а вот под всем остальным скорее подпишусь. Лучше начинать проекты с рекомендуемых тулз вместо исторически самых популярных.

https://python.plainenglish.io/5-overrated-python-libraries-and-what-you-should-use-instead-106bd9ded180
#polars #pandas #codegems

Что в поларс НЕ понравилось.

Активное переименование методов от версии к версии. Это сводит с ума все AI, которые пробуешь использовать для генерации кода.

Мало примеров в доке к сложным методам типа .over и .group_by, .group_by_dynamic. Реально непонятно, как и на каких принципах оно отработает. В документации расписано слабовато.

Иногда polars начинает жрать всю память на относительно простых операциях, где мог бы быть более экономным. Иногда проскакивают ошибки/панические атаки, зачастую сложновоспроизводимые.

И самое главное. Если у вас большие фреймы и много операций (математических, group_by), поларс отработает быстро, но гарантированно забьёт вам всю оперативку. То же сделает и пандас, но за ним неиспользованную память можно хотя бы (если повезёт) подчистить вызовом ctypes.CDLL("libc.so.6").malloc_trim(0)

Поларс же, похоже, создаёт какие-то маленькие и разбросанные в адресном пространстве арены памяти, которые таким способом не подчищаются. И вообще никаким не подчищаются, только завершением процесса. К примеру, у меня после интенсивных расчётов и джойнов фрейм в 100 гигов, а занято памяти на терабайт. И освободить её на линуксе никак нельзя, я уже и аллокаторы пробовал альтернативные типа jemalloc, tmalloc - без толку. Это я считаю большой проблемой вычислений и big data, и странно, что никого это не колышет особо. По факту сбора мусора в линукс нет, если вы не знали, ребята. По крайней мере после отработки поларса. "Спасибо" странным авторам аллокаторов памяти.

Что забавно, на винде этот вопрос решается!!! )) Используйте

def trim_windows_process_memory(pid: int = None) -> bool:
"""Causes effect similar to malloc_trim on -nix."""

# Define SIZE_T based on the platform (32-bit or 64-bit)
if ctypes.sizeof(ctypes.c_void_p) == 4:
SIZE_T = ctypes.c_uint32
else:
SIZE_T = ctypes.c_uint64

# Get a handle to the current process
if not pid:
pid = ctypes.windll.kernel32.GetCurrentProcess()

# Define argument and return types for SetProcessWorkingSetSizeEx
ctypes.windll.kernel32.SetProcessWorkingSetSizeEx.argtypes = [
ctypes.wintypes.HANDLE, # Process handle
SIZE_T, # Minimum working set size
SIZE_T, # Maximum working set size
ctypes.wintypes.DWORD, # Flags
]
ctypes.windll.kernel32.SetProcessWorkingSetSizeEx.restype = ctypes.wintypes.BOOL

# Define constants for SetProcessWorkingSetSizeEx
QUOTA_LIMITS_HARDWS_MIN_DISABLE = 0x00000002

# Attempt to set the working set size
result = ctypes.windll.kernel32.SetProcessWorkingSetSizeEx(pid, SIZE_T(-1), SIZE_T(-1), QUOTA_LIMITS_HARDWS_MIN_DISABLE)

if result == 0:
# Retrieve the error code
error_code = ctypes.windll.kernel32.GetLastError()
logger.error(f"SetProcessWorkingSetSizeEx failed with error code: {error_code}")
return False
else:
return True


и спите спокойно.

Следующая жёсткая проблема - производительность с dtype=category. Не храните паркетные файлы с этим типом данных, лучше конвертируйте в string (с compression='zstd', конечно). Иначе потом они будут склеиваться ВЕЧНОСТЬ, при любых настройках StringCache.
#python #codegems

Как узнать, кто тянет вас на дно?! )

python -X importtime -c "from mymodule import *" 2>import_times.txt
#polars #deltalake #orjson #codegems

Попробовал deltalake в решении по сбору данных. отстой, лучше бы любую СУБД заюзал типа постгре или даже монго. Некоторые выводы из мини-проекта:

1) orjson is x20 faster than json

2) xxhash.xxh128 is x6 faster than hashlib.sha256

3) deltalake package is (at least so far) the toy solution. does not support concurrent writes, I had to use manual locking. with many small updates, requires frequent tables "re-optimizing". i just needed a "primary key" functionality from it - and it's slow, while spending LOTS of CPU. I should have better used any RDBMS, or mongo, instead.

В каком случае deltalake можно использовать: когда записываете данные редко, и с таблицей работает один поток. Либо хочется хостить данные в облачном хранилище типа gcp напрямую в паркете. Еще можно воспользоваться полуручным локом на время операций с дельта таблицей:

import os
import logging
from urllib.parse import urlparse
from filelock import FileLock, Timeout

logger = logging.getLogger(__name__)


def is_local_path(path: str) -> bool:
parsed = urlparse(path)
# If there's no scheme or it's explicitly "file"
if parsed.scheme in ("", "file"):
return not path.startswith(("s3://", "azure://"))

# Special case: Windows drive letter (e.g., "R:\...")
if os.name == "nt" and len(parsed.scheme) == 1 and parsed.scheme.isalpha():
return True

return False


def safe_delta_write(path: str, delta_op_func, *, lock_timeout: int = 120, lock_suffix=".lock"):
"""
Wraps any Delta Lake operation (write_deltalake, merge+execute) with local file locking.

Parameters:
path (str): Delta table path.
delta_op_func (callable): A function that performs the actual Delta operation.
lock_timeout (int): How many seconds to wait for the lock before skipping.
lock_suffix (str): Suffix for the lock filename.

Usage Examples
🔁 For .merge().when_not_matched_insert_all().execute():

def merge_ads_static():
return DeltaTable(ADS_STATIC_PATH).merge(
static_df,
predicate="t.id = s.id",
source_alias="s",
target_alias="t",
writer_properties=DELTALAKE_OPTIONS.get("writer_properties")
).when_not_matched_insert_all().execute()

safe_delta_write(ADS_STATIC_PATH, merge_ads_static)

📝 For write_deltalake() appends:

def write_market_ads():
return write_deltalake(
MARKET_ADS_PATH,
market_df,
mode="append",
partition_by=["date"],
**DELTALAKE_OPTIONS
)

safe_delta_write(MARKET_ADS_PATH, write_market_ads)
"""
if is_local_path(path):
lock_file = os.path.join("/tmp", f"{os.path.basename(path).replace('/', '_')}{lock_suffix}")
lock = FileLock(lock_file)

try:
with lock.acquire(timeout=lock_timeout):
logger.debug(f"Acquired lock for local Delta path: {path}")
return delta_op_func()
except Timeout:
logger.warning(f"Timeout while waiting for lock on {path}. Skipping operation.")
except Exception as e:
logger.exception(f"Delta operation failed on {path}: {e}")
raise (e)
else:
logger.warning(f"Delta operation on non-local path: {path}. Proceeding without lock.")
try:
return delta_op_func()
except Exception as e:
logger.exception(f"Delta operation failed on {path}: {e}")