Спочатку ми отримаємо деякі дані про затримку рейсів.
from pathlib import Pathfrom zipfile import ZipFileimport requestsdata_dir = Path("../data") # replace this with a directory of your choicedest = data_dir /"flights.csv.zip"ifnot dest.exists(): r = requests.get("https://transtats.bts.gov/PREZIP/On_Time_Reporting_Carrier_On_Time_Performance_1987_present_2022_1.zip", verify=False, stream=True, ) data_dir.mkdir(exist_ok=True)with dest.open("wb") as f:for chunk in r.iter_content(chunk_size=102400):if chunk: f.write(chunk)with ZipFile(dest) as zf: zf.extract(zf.filelist[0].filename, path=data_dir)extracted = data_dir /"On_Time_Reporting_Carrier_On_Time_Performance_(1987_present)_2022_1.csv"
Дані з Google Drive
import gdownimport ioimport polars as plimport pandas as pdimport timefile_id ="1UApDVdpgyeeZbJTvgCIjMjumGv_tWeQi"url =f"https://drive.google.com/uc?id={file_id}"response = gdown.download(url, quiet=True, fuzzy=True)
def extract_city_name_pl() -> pl.Expr:""" Chicago, IL -> Chicago for OriginCityName and DestCityName """ cols = ["OriginCityName", "DestCityName"]return pl.col(cols).str.split(",").list.get(0)
def extract_city_name_pd(df: pd.DataFrame) -> pl.DataFrame:""" Chicago, IL -> Chicago for OriginCityName and DestCityName """ cols = ["OriginCityName", "DestCityName"]return df.assign(**{col: df[col].str.split(",", regex=False).str[0] for col in cols})
Отримати назви міст
Кілька пунктів, на які слід звернути увагу:
Наша функція Pandas додає стовпці до фрейму даних, тоді як наша функція Polars просто генерує вираз. Ви побачите, що часто простіше передавати Expr, ніж фрейми даних, тому що
Вони працюють як з DataFrame, так і з LazyFrame, і вони не прив’язані до конкретних даних.
Polars працює краще, якщо ви поміщаєте все в один виклик .select або .with_columns, а не викликаєте .select кілька разів. Якщо ви передаєте вирази, то цей патерн стає простим.
Polars є швидким і зручним для виконання одних і тих же дій з декількома стовпчиками. Ми можемо передати список стовпців в pl.col а потім викликати метод на цьому pl.col так, ніби це один стовпець. Коли вираз буде виконано, його буде розпаралелено за допомогою Polars.
Тим часом у Pandas нам доводиться циклічно перебирати стовпці, щоб створити словник kwargs для .assign. Це не розпаралелюється. (Ми могли б використовувати .apply з axis=0 замість цього, але це все одно відбуватиметься послідовно).
Виклик .str.split у Polars створює стовпчик, де кожен елемент є списком. Такий тип даних дратує в Pandas тому що з ними повільно і незручно працювати - зверніть увагу, що найзручніший спосіб отримати перший елемент стовпця зі списком у Pandas - це викликати .str[0], навіть якщо це список, а не рядок 🤔.
Я не впевнений, що це взагалі має працювати. На противагу цьому, Polars насправді має першокласну підтримку стовпців зі списками, і вони працюють швидко, якщо вони не мають змішаних типів.
dtypes_pl = ( {col: pl.Categorical for col in category_cols}| {"FlightDate": pl.Date}| {col: pl.Utf8 for col in time_cols})df_pl = ( pl.scan_csv(extracted, schema_overrides=dtypes_pl, null_values="") .select(cols) .with_columns([extract_city_name_pl(), *time_to_datetime_pl(time_cols)]) .collect())df_pl.head()
shape: (5, 14)
Dest
Tail_Number
IATA_CODE_Reporting_Airline
CancellationCode
DepTime
ArrTime
CRSArrTime
CRSDepTime
FlightDate
Flight_Number_Reporting_Airline
OriginCityName
DestCityName
Origin
DepDelay
cat
cat
cat
cat
datetime[μs]
datetime[μs]
datetime[μs]
datetime[μs]
date
i64
str
str
str
f64
"DCA"
"N119HQ"
"YX"
null
2022-01-14 12:21:00
2022-01-14 13:56:00
2022-01-14 13:52:00
2022-01-14 12:24:00
2022-01-14
4879
"Columbus"
"Washington"
"CMH"
-3.0
"DCA"
"N122HQ"
"YX"
null
2022-01-15 12:14:00
2022-01-15 13:28:00
2022-01-15 13:52:00
2022-01-15 12:24:00
2022-01-15
4879
"Columbus"
"Washington"
"CMH"
-10.0
"DCA"
"N412YX"
"YX"
null
2022-01-16 12:18:00
2022-01-16 13:39:00
2022-01-16 13:52:00
2022-01-16 12:24:00
2022-01-16
4879
"Columbus"
"Washington"
"CMH"
-6.0
"DCA"
"N405YX"
"YX"
null
2022-01-17 12:17:00
2022-01-17 14:01:00
2022-01-17 13:52:00
2022-01-17 12:24:00
2022-01-17
4879
"Columbus"
"Washington"
"CMH"
-7.0
"DCA"
"N420YX"
"YX"
null
2022-01-18 12:18:00
2022-01-18 13:23:00
2022-01-18 13:52:00
2022-01-18 12:24:00
2022-01-18
4879
"Columbus"
"Washington"
"CMH"
-6.0
dtypes_pd = ( {col: pd.CategoricalDtype() for col in category_cols}| {col: pd.StringDtype() for col in time_cols})df_pd = ( pd.read_csv(extracted, dtype=dtypes_pd, usecols=cols, na_values="") .pipe(extract_city_name_pd) .pipe(time_to_datetime_pd, time_cols) .assign(FlightDate=lambda df: pd.to_datetime(df["FlightDate"])))df_pd[cols].head()
Dest
Tail_Number
IATA_CODE_Reporting_Airline
CancellationCode
DepTime
ArrTime
CRSArrTime
CRSDepTime
FlightDate
Flight_Number_Reporting_Airline
OriginCityName
DestCityName
Origin
DepDelay
0
DCA
N119HQ
YX
NaN
2022-01-14 12:21:00
2022-01-14 13:56:00
2022-01-14 13:52:00
2022-01-14 12:24:00
2022-01-14
4879
Columbus
Washington
CMH
-3.0
1
DCA
N122HQ
YX
NaN
2022-01-15 12:14:00
2022-01-15 13:28:00
2022-01-15 13:52:00
2022-01-15 12:24:00
2022-01-15
4879
Columbus
Washington
CMH
-10.0
2
DCA
N412YX
YX
NaN
2022-01-16 12:18:00
2022-01-16 13:39:00
2022-01-16 13:52:00
2022-01-16 12:24:00
2022-01-16
4879
Columbus
Washington
CMH
-6.0
3
DCA
N405YX
YX
NaN
2022-01-17 12:17:00
2022-01-17 14:01:00
2022-01-17 13:52:00
2022-01-17 12:24:00
2022-01-17
4879
Columbus
Washington
CMH
-7.0
4
DCA
N420YX
YX
NaN
2022-01-18 12:18:00
2022-01-18 13:23:00
2022-01-18 13:52:00
2022-01-18 12:24:00
2022-01-18
4879
Columbus
Washington
CMH
-6.0
Використовуємо функції
Відмінності між двома підходами:
Оскільки scan_csv є лінивим, використання scan_csv з наступним .select для вибору підмножини стовпців еквівалентно usecols у pd.read_csv. Ось чому сам pl.scan_csv не має параметра для вибору підмножини стовпців для читання.
У Polars є метод .pipe, але ми не використовуємо його у цьому випадку, оскільки простіше працювати з виразами.
# filter for the busiest airlinesfilter_expr = pl.col("IATA_CODE_Reporting_Airline").is_in( pl.col("IATA_CODE_Reporting_Airline") .value_counts(sort=True) .struct.field("IATA_CODE_Reporting_Airline") .head(5))( df_pl .drop_nulls(subset=["DepTime", "IATA_CODE_Reporting_Airline"]) .filter(filter_expr) .sort("DepTime") .group_by_dynamic("DepTime", every="1h", group_by="IATA_CODE_Reporting_Airline") .agg(pl.col("Flight_Number_Reporting_Airline").count()) .pivot( index="DepTime", on="IATA_CODE_Reporting_Airline", values="Flight_Number_Reporting_Airline", ) .sort("DepTime")# fill every missing hour with 0 so the plot looks better .upsample(time_column="DepTime", every="1h") .fill_null(0) .select([pl.col("DepTime"), pl.col(pl.UInt32).rolling_sum(24)]) .to_pandas() .set_index("DepTime") .rename_axis("Flights per Day", axis=1) .plot())
( df_pd .dropna(subset=["DepTime", "IATA_CODE_Reporting_Airline"])# filter for the busiest airlines .loc[lambda x: x["IATA_CODE_Reporting_Airline"].isin( x["IATA_CODE_Reporting_Airline"].value_counts().index[:5] ) ] .assign( IATA_CODE_Reporting_Airline=lambda x: x["IATA_CODE_Reporting_Airline" ].cat.remove_unused_categories() # annoying pandas behaviour ) .set_index("DepTime")# TimeGrouper to resample & groupby at once .groupby(["IATA_CODE_Reporting_Airline", pd.Grouper(freq="h")])["Flight_Number_Reporting_Airline" ] .count()# the .pivot takes care of this in the Polars code. .unstack(0) .fillna(0) .rolling(24) .sum() .rename_axis("Flights per Day", axis=1) .plot())
Візуалізація
Відмінності між Polars і Pandas:
Для групування за часовим вікном та іншим значенням ми використовуємо .groupby_dynamic. У Pandas ми використовуємо .groupby з помічником pd.Grouper.
Замість .rolling(n).sum() у Polars використовується .rolling_sum(n).
Якщо ви бачите код Pandas, що використовує .unstack, то відповідний код Polars, ймовірно, потребує .pivot.
У Polars .value_counts повертає стовпець pl.Struct, що містить значення та кількість значень. У Pandas вона повертає ряд, де елементами є кількість значень, а індекс містить самі значення.
У Polars нам потрібно вибрати всі стовпці UInt32 в одній точці за допомогою pl.col(pl.UInt32). У Pandas спосіб роботи .rolling означає, що нам не потрібно вибирати ці стовпці явно, але якщо ми це зробимо, це буде виглядати як df.select_dtypes("uint32").
Використовуйте Expr і не використовуйте .apply, якщо це дійсно необхідно.
Використовуйте найменші необхідні числові типи (наприклад, якщо у вас є ціле число від 0 до 255, використовуйте pl.UInt8, а не pl.Int64). Це заощадить і час, і місце.
Використовуйте ефективне сховище (якщо ви зберігаєте дані у файлах, Parquet - хороший вибір).
Використовуйте категоризації для рядків, що повторюються (але зауважте, що це може бути недоцільно, якщо повторюваність невелика).
# не можна використовувати UInt8/16 в scan_csvdtypes_pl = ( {col: pl.Utf8 for col in initially_str_cols_pl}| {col: pl.Categorical for col in initial_category_cols_pl}| {col: pl.UInt32 for col in [*u32_cols, *u16_cols, *u8_cols]})
dtypes_pd = ( {col: pd.StringDtype() for col in initially_str_cols}| {col: pd.CategoricalDtype() for col in category_cols}| {col: "uint32"for col in u32_cols}| {col: "uint8"for col in u8_cols}| {col: "uint16"for col in u16_cols})
%%timenew_cols_pl = ([ pl.col("Club").str.strip_chars().cast(pl.Categorical), parse_suffixed_num_pl(pl.col("Hits")).cast(pl.UInt32), pl.col("Positions").str.split(","), parse_height_pl(pl.col("Height")), parse_weight_pl(pl.col("Weight")),]+ [parse_date_pl(pl.col(col)) for col in date_cols]+ [parse_money_pl(pl.col(col)) for col in money_cols]+ [parse_star_pl(pl.col(col)) for col in star_cols]+ parse_contract_pl(pl.col("Contract"))+ [pl.col(col).cast(pl.UInt16) for col in u16_cols]+ [pl.col(col).cast(pl.UInt8) for col in u8_cols])fifa_pl = ( pl.scan_csv("data/fifa21_raw_big.csv", schema_overrides=dtypes_pl) .with_columns(new_cols_pl) .drop("Contract") .rename({"↓OVA": "OVA"}) .collect())
CPU times: total: 1.16 s
Wall time: 214 ms
%%timefifa_pd = ( pd.read_csv("data/fifa21_raw_big.csv", dtype=dtypes_pd) .assign(Club=lambda df: df["Club"].cat.rename_categories(lambda c: c.strip()),**{col: lambda df: parse_date_pd(df[col]) for col in date_cols},**{col: lambda df: parse_money_pd(df[col]) for col in money_cols},**{col: lambda df: parse_star_pd(df[col]) for col in star_cols}, Hits=lambda df: parse_suffixed_num_pd(df["Hits"]).astype(pd.UInt32Dtype()), Positions=lambda df: df["Positions"].str.split(","), Height=lambda df: parse_height_pd(df["Height"]), Weight=lambda df: parse_weight_pd(df["Weight"]) ) .pipe(parse_contract_pd) .rename(columns={"↓OVA": "OVA"}))
751 ms ± 14.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit pandas_transform(rand_df_pd)
1.19 s ± 127 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Охайні дані
Охайні дані
Існує ціла стаття Хедлі Вікхем (Hadley Wickham) про чисті дані, але вона у форматі PDF, тому ви, мабуть, не будете її читати. Ось визначення чистих даних, наведене в цій статті:
Кожна змінна утворює стовпець.
Кожне спостереження формує рядок.
Кожен тип одиниці спостереження формує таблицю.
Дані NBA
from pathlib import Pathimport polars as plimport pandas as pdpl.Config.set_tbl_rows(5)pd.options.display.max_rows =5nba_dir = Path("data/nba/")column_names = {"Date": "date","Visitor/Neutral": "away_team","PTS": "away_points","Home/Neutral": "home_team","PTS.1": "home_points",}ifnot nba_dir.exists(): nba_dir.mkdir()for month in ("october","november","december","january","february","march","april","may","june", ):# На практиці ми б зробили більше очищення даних тут, і зберегли б у паркет, а не CSV.# Але ми зберігаємо брудні дані тут, щоб потім очистити їх для педагогічних цілей. url =f"http://www.basketball-reference.com/leagues/NBA_2016_games-{month}.html" tables = pd.read_html(url) raw = ( pl.from_pandas(tables[0].query("Date != 'Playoffs'")) .rename(column_names) .select(column_names.values()) ) raw.write_csv(nba_dir /f"{month}.csv")nba_glob = nba_dir /"*.csv"pl.scan_csv(nba_glob).head().collect()
Припустимо, ми хочемо порахувати дні відпочинку кожної команди перед кожною грою. У поточній структурі це складно, оскільки нам потрібно відстежувати обидва стовпці home_team і away_team