polars-df 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/Cargo.lock +270 -97
  4. data/LICENSE.txt +1 -1
  5. data/README.md +1 -3
  6. data/ext/polars/Cargo.toml +19 -18
  7. data/ext/polars/src/catalog/unity.rs +15 -20
  8. data/ext/polars/src/conversion/any_value.rs +53 -29
  9. data/ext/polars/src/conversion/chunked_array.rs +58 -56
  10. data/ext/polars/src/conversion/datetime.rs +58 -7
  11. data/ext/polars/src/conversion/mod.rs +200 -150
  12. data/ext/polars/src/dataframe/export.rs +15 -12
  13. data/ext/polars/src/dataframe/general.rs +25 -7
  14. data/ext/polars/src/dataframe/map.rs +6 -4
  15. data/ext/polars/src/error.rs +1 -1
  16. data/ext/polars/src/expr/array.rs +0 -24
  17. data/ext/polars/src/expr/datatype.rs +13 -3
  18. data/ext/polars/src/expr/datetime.rs +4 -4
  19. data/ext/polars/src/expr/general.rs +35 -15
  20. data/ext/polars/src/expr/list.rs +0 -26
  21. data/ext/polars/src/expr/rolling.rs +24 -0
  22. data/ext/polars/src/functions/business.rs +2 -2
  23. data/ext/polars/src/functions/io.rs +4 -3
  24. data/ext/polars/src/functions/lazy.rs +65 -46
  25. data/ext/polars/src/functions/meta.rs +6 -5
  26. data/ext/polars/src/functions/mod.rs +0 -1
  27. data/ext/polars/src/functions/range.rs +13 -0
  28. data/ext/polars/src/functions/utils.rs +4 -2
  29. data/ext/polars/src/interop/arrow/mod.rs +4 -2
  30. data/ext/polars/src/interop/arrow/to_rb.rs +1 -1
  31. data/ext/polars/src/interop/numo/to_numo_series.rs +26 -25
  32. data/ext/polars/src/io/scan_options.rs +6 -3
  33. data/ext/polars/src/io/sink_options.rs +2 -0
  34. data/ext/polars/src/lazyframe/general.rs +243 -17
  35. data/ext/polars/src/lazyframe/optflags.rs +2 -1
  36. data/ext/polars/src/lib.rs +39 -35
  37. data/ext/polars/src/map/lazy.rs +5 -2
  38. data/ext/polars/src/map/series.rs +19 -18
  39. data/ext/polars/src/on_startup.rs +25 -6
  40. data/ext/polars/src/ruby/numo.rs +3 -4
  41. data/ext/polars/src/ruby/plan_callback.rs +1 -4
  42. data/ext/polars/src/ruby/rb_modules.rs +2 -4
  43. data/ext/polars/src/ruby/ruby_udf.rs +7 -9
  44. data/ext/polars/src/ruby/utils.rs +12 -1
  45. data/ext/polars/src/series/aggregation.rs +13 -1
  46. data/ext/polars/src/series/construction.rs +31 -50
  47. data/ext/polars/src/series/export.rs +33 -38
  48. data/ext/polars/src/series/general.rs +6 -6
  49. data/ext/polars/src/series/map.rs +3 -2
  50. data/ext/polars/src/series/scatter.rs +4 -4
  51. data/ext/polars/src/utils.rs +31 -7
  52. data/lib/polars/array_expr.rb +23 -7
  53. data/lib/polars/array_name_space.rb +16 -2
  54. data/lib/polars/binary_name_space.rb +32 -0
  55. data/lib/polars/collect_batches.rb +4 -0
  56. data/lib/polars/data_frame.rb +144 -11
  57. data/lib/polars/data_type_group.rb +5 -0
  58. data/lib/polars/date_time_expr.rb +91 -3
  59. data/lib/polars/date_time_name_space.rb +7 -1
  60. data/lib/polars/expr.rb +247 -44
  61. data/lib/polars/functions/business.rb +2 -2
  62. data/lib/polars/functions/datatype.rb +30 -0
  63. data/lib/polars/functions/eager.rb +80 -7
  64. data/lib/polars/functions/lazy.rb +97 -2
  65. data/lib/polars/functions/range/linear_space.rb +118 -0
  66. data/lib/polars/io/csv.rb +27 -5
  67. data/lib/polars/io/database.rb +2 -3
  68. data/lib/polars/io/ipc.rb +2 -2
  69. data/lib/polars/io/lines.rb +172 -0
  70. data/lib/polars/io/parquet.rb +1 -1
  71. data/lib/polars/io/sink_options.rb +5 -2
  72. data/lib/polars/lazy_frame.rb +517 -14
  73. data/lib/polars/list_expr.rb +21 -7
  74. data/lib/polars/list_name_space.rb +16 -2
  75. data/lib/polars/query_opt_flags.rb +23 -5
  76. data/lib/polars/selectors.rb +2 -2
  77. data/lib/polars/series.rb +176 -19
  78. data/lib/polars/sql_context.rb +2 -2
  79. data/lib/polars/string_cache.rb +19 -72
  80. data/lib/polars/string_expr.rb +1 -7
  81. data/lib/polars/string_name_space.rb +1 -7
  82. data/lib/polars/utils/construction/series.rb +24 -39
  83. data/lib/polars/utils/convert.rb +16 -6
  84. data/lib/polars/utils/parse.rb +7 -0
  85. data/lib/polars/utils/reduce_balanced.rb +43 -0
  86. data/lib/polars/utils/various.rb +5 -0
  87. data/lib/polars/version.rb +1 -1
  88. data/lib/polars.rb +2 -1
  89. metadata +4 -17
  90. data/ext/polars/src/functions/string_cache.rs +0 -24
data/lib/polars/expr.rb CHANGED
@@ -252,6 +252,38 @@ module Polars
252
252
  wrap_expr(_rbexpr.all(ignore_nulls))
253
253
  end
254
254
 
255
+ # Return whether the column is empty.
256
+ #
257
+ # @note
258
+ # This functionality is considered **unstable**. It may be changed
259
+ # at any point without it being considered a breaking change.
260
+ #
261
+ # @param ignore_nulls [Boolean]
262
+ # If true a column containing only nulls will also be considered empty.
263
+ # The default is false.
264
+ #
265
+ # @return [Expr]
266
+ #
267
+ # @example
268
+ # df = Polars::DataFrame.new({"x" => [nil, nil]})
269
+ # df.select(
270
+ # a: Polars.col("x").is_empty,
271
+ # b: Polars.col("x").drop_nulls.is_empty,
272
+ # c: Polars.col("x").is_empty(ignore_nulls: true)
273
+ # )
274
+ # # =>
275
+ # # shape: (1, 3)
276
+ # # ┌───────┬──────┬──────┐
277
+ # # │ a ┆ b ┆ c │
278
+ # # │ --- ┆ --- ┆ --- │
279
+ # # │ bool ┆ bool ┆ bool │
280
+ # # ╞═══════╪══════╪══════╡
281
+ # # │ false ┆ true ┆ true │
282
+ # # └───────┴──────┴──────┘
283
+ def is_empty(ignore_nulls: false)
284
+ wrap_expr(_rbexpr.is_empty(ignore_nulls))
285
+ end
286
+
255
287
  # Return indices where expression evaluates `true`.
256
288
  #
257
289
  # @return [Expr]
@@ -1204,6 +1236,49 @@ module Polars
1204
1236
  wrap_expr(_rbexpr.round_sig_figs(digits))
1205
1237
  end
1206
1238
 
1239
+ # Truncate numeric data toward zero to `decimals` number of decimal places.
1240
+ #
1241
+ # @param decimals [Integer]
1242
+ # Number of decimal places to truncate to.
1243
+ #
1244
+ # @return [Expr]
1245
+ #
1246
+ # @note
1247
+ # Truncation discards the fractional part beyond the given number of decimals.
1248
+ # For example, when rounding to 0 decimals 0.25, -0.25, 0.99, and -0.99 will
1249
+ # all round to 0. When rounding to 1 decimal 1.9999 rounds to 1.9 and -1.9999
1250
+ # rounds to -1.9. There is no tiebreak behaviour at midpoint values as there
1251
+ # is with :meth:`round` so 0.5 and -0.5 will also round to 0 when decimals=1.
1252
+ #
1253
+ # @note
1254
+ # This method performs numeric truncation. For truncating temporal
1255
+ # data (dates/datetimes), use :func:`Expr.dt.truncate` instead.
1256
+ #
1257
+ # @example
1258
+ # df = Polars::DataFrame.new({"n" => [-9.9999, 0.12345, 1.0251, 8.8765]})
1259
+ # df.with_columns(
1260
+ # t0: Polars.col("n").truncate(0),
1261
+ # t1: Polars.col("n").truncate(1),
1262
+ # t2: Polars.col("n").truncate(2),
1263
+ # t3: Polars.col("n").truncate(3),
1264
+ # t4: Polars.col("n").truncate(4)
1265
+ # )
1266
+ # # =>
1267
+ # # shape: (4, 6)
1268
+ # # ┌─────────┬──────┬──────┬───────┬────────┬─────────┐
1269
+ # # │ n ┆ t0 ┆ t1 ┆ t2 ┆ t3 ┆ t4 │
1270
+ # # │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
1271
+ # # │ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
1272
+ # # ╞═════════╪══════╪══════╪═══════╪════════╪═════════╡
1273
+ # # │ -9.9999 ┆ -9.0 ┆ -9.9 ┆ -9.99 ┆ -9.999 ┆ -9.9999 │
1274
+ # # │ 0.12345 ┆ 0.0 ┆ 0.1 ┆ 0.12 ┆ 0.123 ┆ 0.1234 │
1275
+ # # │ 1.0251 ┆ 1.0 ┆ 1.0 ┆ 1.02 ┆ 1.025 ┆ 1.025 │
1276
+ # # │ 8.8765 ┆ 8.0 ┆ 8.8 ┆ 8.87 ┆ 8.876 ┆ 8.8765 │
1277
+ # # └─────────┴──────┴──────┴───────┴────────┴─────────┘
1278
+ def truncate(decimals = 0)
1279
+ Utils.wrap_expr(_rbexpr.truncate(decimals))
1280
+ end
1281
+
1207
1282
  # Compute the dot/inner product between two Expressions.
1208
1283
  #
1209
1284
  # @param other [Expr]
@@ -1887,6 +1962,11 @@ module Polars
1887
1962
  #
1888
1963
  # @param indices [Expr]
1889
1964
  # An expression that leads to a `:u32` dtyped Series.
1965
+ # @param null_on_oob [Boolean]
1966
+ # Behavior if an index is out of bounds:
1967
+ #
1968
+ # - true -> set the result to null
1969
+ # - false -> raise an error
1890
1970
  #
1891
1971
  # @return [Expr]
1892
1972
  #
@@ -1915,13 +1995,13 @@ module Polars
1915
1995
  # # │ one ┆ [2, 98] │
1916
1996
  # # │ two ┆ [4, 99] │
1917
1997
  # # └───────┴───────────┘
1918
- def gather(indices)
1998
+ def gather(indices, null_on_oob: false)
1919
1999
  if indices.is_a?(::Array)
1920
2000
  indices_lit_rbexpr = Polars.lit(Series.new("", indices, dtype: Int64))._rbexpr
1921
2001
  else
1922
2002
  indices_lit_rbexpr = Utils.parse_into_expression(indices)
1923
2003
  end
1924
- wrap_expr(_rbexpr.gather(indices_lit_rbexpr))
2004
+ wrap_expr(_rbexpr.gather(indices_lit_rbexpr, null_on_oob))
1925
2005
  end
1926
2006
 
1927
2007
  # Return a single value by index.
@@ -2266,6 +2346,38 @@ module Polars
2266
2346
  wrap_expr(_rbexpr.max)
2267
2347
  end
2268
2348
 
2349
+ # Get maximum value, ordered by another expression.
2350
+ #
2351
+ # If the by expression has multiple values equal to the maximum it is not
2352
+ # defined which value will be chosen.
2353
+ #
2354
+ # @note
2355
+ # This functionality is considered **unstable**. It may be changed
2356
+ # at any point without it being considered a breaking change.
2357
+ #
2358
+ # @param by [Object]
2359
+ # Column used to determine the largest element.
2360
+ # Accepts expression input. Strings are parsed as column names.
2361
+ #
2362
+ # @return [Expr]
2363
+ #
2364
+ # @example
2365
+ # df = Polars::DataFrame.new({"a" => [-1.0, Float::NAN, 1.0], "b" => ["x", "y", "z"]})
2366
+ # df.select(Polars.col("b").max_by("a"))
2367
+ # # =>
2368
+ # # shape: (1, 1)
2369
+ # # ┌─────┐
2370
+ # # │ b │
2371
+ # # │ --- │
2372
+ # # │ str │
2373
+ # # ╞═════╡
2374
+ # # │ z │
2375
+ # # └─────┘
2376
+ def max_by(by)
2377
+ by_rbexpr = Utils.parse_into_expression(by)
2378
+ wrap_expr(_rbexpr.max_by(by_rbexpr))
2379
+ end
2380
+
2269
2381
  # Get minimum value.
2270
2382
  #
2271
2383
  # @return [Expr]
@@ -2286,6 +2398,38 @@ module Polars
2286
2398
  wrap_expr(_rbexpr.min)
2287
2399
  end
2288
2400
 
2401
+ # Get minimum value, ordered by another expression.
2402
+ #
2403
+ # If the by expression has multiple values equal to the minimum it is not
2404
+ # defined which value will be chosen.
2405
+ #
2406
+ # @note
2407
+ # This functionality is considered **unstable**. It may be changed
2408
+ # at any point without it being considered a breaking change.
2409
+ #
2410
+ # @param by [Object]
2411
+ # Column used to determine the smallest element.
2412
+ # Accepts expression input. Strings are parsed as column names.
2413
+ #
2414
+ # @return [Expr]
2415
+ #
2416
+ # @example
2417
+ # df = Polars::DataFrame.new({"a" => [-1.0, Float::NAN, 1.0], "b" => ["x", "y", "z"]})
2418
+ # df.select(Polars.col("b").min_by("a"))
2419
+ # # =>
2420
+ # # shape: (1, 1)
2421
+ # # ┌─────┐
2422
+ # # │ b │
2423
+ # # │ --- │
2424
+ # # │ str │
2425
+ # # ╞═════╡
2426
+ # # │ x │
2427
+ # # └─────┘
2428
+ def min_by(by)
2429
+ by_rbexpr = Utils.parse_into_expression(by)
2430
+ wrap_expr(_rbexpr.min_by(by_rbexpr))
2431
+ end
2432
+
2289
2433
  # Get maximum value, but propagate/poison encountered NaN values.
2290
2434
  #
2291
2435
  # @return [Expr]
@@ -3131,17 +3275,17 @@ module Polars
3131
3275
  # )
3132
3276
  # # =>
3133
3277
  # # shape: (5, 2)
3134
- # # ┌─────┬─────┐
3135
- # # │ foo ┆ cut
3136
- # # │ --- ┆ ---
3137
- # # │ i64 ┆ cat
3138
- # # ╞═════╪═════╡
3139
- # # │ -2 ┆ a
3140
- # # │ -1 ┆ a
3141
- # # │ 0 ┆ b
3142
- # # │ 1 ┆ b
3143
- # # │ 2 ┆ c
3144
- # # └─────┴─────┘
3278
+ # # ┌─────┬──────┐
3279
+ # # │ foo ┆ cut
3280
+ # # │ --- ┆ ---
3281
+ # # │ i64 ┆ enum
3282
+ # # ╞═════╪══════╡
3283
+ # # │ -2 ┆ a
3284
+ # # │ -1 ┆ a
3285
+ # # │ 0 ┆ b
3286
+ # # │ 1 ┆ b
3287
+ # # │ 2 ┆ c
3288
+ # # └─────┴──────┘
3145
3289
  #
3146
3290
  # @example Add both the category and the breakpoint.
3147
3291
  # df.with_columns(
@@ -3152,7 +3296,7 @@ module Polars
3152
3296
  # # ┌─────┬────────────┬────────────┐
3153
3297
  # # │ foo ┆ breakpoint ┆ category │
3154
3298
  # # │ --- ┆ --- ┆ --- │
3155
- # # │ i64 ┆ f64 ┆ cat
3299
+ # # │ i64 ┆ f64 ┆ enum
3156
3300
  # # ╞═════╪════════════╪════════════╡
3157
3301
  # # │ -2 ┆ -1.0 ┆ (-inf, -1] │
3158
3302
  # # │ -1 ┆ -1.0 ┆ (-inf, -1] │
@@ -4502,13 +4646,18 @@ module Polars
4502
4646
  wrap_expr(_rbexpr._hash(k0, k1, k2, k3))
4503
4647
  end
4504
4648
 
4505
- # Reinterpret the underlying bits as a signed/unsigned integer.
4649
+ # Reinterpret the underlying bits as a signed/unsigned integer or float.
4650
+ #
4651
+ # This operation is only allowed for numeric types of the same size.
4652
+ # For lower bits numbers, you can safely use the cast operation.
4506
4653
  #
4507
- # This operation is only allowed for 64bit integers. For lower bits integers,
4508
- # you can safely use that cast operation.
4654
+ # Either `signed` or `dtype` can be specified.
4509
4655
  #
4510
4656
  # @param signed [Boolean]
4511
- # If true, reinterpret as `Polars::Int64`. Otherwise, reinterpret as `Polars::UInt64`.
4657
+ # If true, reinterpret as signed integer. Otherwise, reinterpret
4658
+ # as unsigned integer.
4659
+ # @param dtype [Object]
4660
+ # DataType to reinterpret to.
4512
4661
  #
4513
4662
  # @return [Expr]
4514
4663
  #
@@ -4517,7 +4666,7 @@ module Polars
4517
4666
  # df = Polars::DataFrame.new([s])
4518
4667
  # df.select(
4519
4668
  # [
4520
- # Polars.col("a").reinterpret(signed: true).alias("reinterpreted"),
4669
+ # Polars.col("a").reinterpret(dtype: Polars::Int64).alias("reinterpreted"),
4521
4670
  # Polars.col("a").alias("original")
4522
4671
  # ]
4523
4672
  # )
@@ -4532,14 +4681,13 @@ module Polars
4532
4681
  # # │ 1 ┆ 1 │
4533
4682
  # # │ 2 ┆ 2 │
4534
4683
  # # └───────────────┴──────────┘
4535
- def reinterpret(signed: nil)
4536
- # TODO update
4537
- if signed.nil?
4538
- warn "The default `signed` for `reinterpret` method will change from `false` to `true` in a future version"
4539
- signed = false
4684
+ def reinterpret(signed: nil, dtype: nil)
4685
+ if signed.nil? == dtype.nil?
4686
+ msg = "reinterpret requires exactly one of `signed` or `dtype` to be specified"
4687
+ raise ArgumentError, msg
4540
4688
  end
4541
4689
 
4542
- wrap_expr(_rbexpr.reinterpret(signed))
4690
+ wrap_expr(_rbexpr.reinterpret(signed, dtype))
4543
4691
  end
4544
4692
 
4545
4693
  # Print the value that this expression evaluates to and pass on the value.
@@ -5111,7 +5259,7 @@ module Polars
5111
5259
  def rolling_sum_by(
5112
5260
  by,
5113
5261
  window_size,
5114
- min_samples: 1,
5262
+ min_samples: 0,
5115
5263
  closed: "right"
5116
5264
  )
5117
5265
  window_size = _prepare_rolling_by_window_args(window_size)
@@ -6486,6 +6634,67 @@ module Polars
6486
6634
  )
6487
6635
  end
6488
6636
 
6637
+ # Compute a custom rolling window function.
6638
+ #
6639
+ # @note
6640
+ # This functionality is considered **unstable**. It may be changed
6641
+ # at any point without it being considered a breaking change.
6642
+ #
6643
+ # @param window_size [Integer]
6644
+ # The length of the window in number of elements.
6645
+ # @param weights [Object]
6646
+ # An optional slice with the same length as the window that will be multiplied
6647
+ # elementwise with the values in the window.
6648
+ # @param min_samples [Integer]
6649
+ # The number of values in the window that should be non-null before computing
6650
+ # a result. If set to `nil` (default), it will be set equal to `window_size`.
6651
+ # @param center [Boolean]
6652
+ # Set the labels at the center of the window.
6653
+ #
6654
+ # @return [Expr]
6655
+ #
6656
+ # @example
6657
+ # df = Polars::DataFrame.new({"a" => [11.0, 2.0, 9.0, Float::NAN, 8.0]})
6658
+ # df.select(Polars.col("a").rolling_map(3) { |v| v.drop_nans.sum })
6659
+ # # =>
6660
+ # # shape: (5, 1)
6661
+ # # ┌──────┐
6662
+ # # │ a │
6663
+ # # │ --- │
6664
+ # # │ f64 │
6665
+ # # ╞══════╡
6666
+ # # │ null │
6667
+ # # │ null │
6668
+ # # │ 22.0 │
6669
+ # # │ 11.0 │
6670
+ # # │ 17.0 │
6671
+ # # └──────┘
6672
+ def rolling_map(
6673
+ window_size,
6674
+ weights: nil,
6675
+ min_samples: nil,
6676
+ center: false,
6677
+ &function
6678
+ )
6679
+ if min_samples.nil?
6680
+ min_samples = window_size
6681
+ end
6682
+
6683
+ _wrap = lambda do |rbs|
6684
+ s = Utils.wrap_s(rbs)
6685
+ rv = function.(s)
6686
+ if rv.is_a?(Series)
6687
+ rv._s
6688
+ else
6689
+ Series.new([rv])._s
6690
+ end
6691
+ end
6692
+
6693
+ wrap_expr(
6694
+ _rbexpr.rolling_map(_wrap, window_size, weights, min_samples, center)
6695
+ )
6696
+ end
6697
+
6489
6698
  # Compute absolute values.
6490
6699
  #
6491
6700
  # @return [Expr]
@@ -7237,17 +7446,11 @@ module Polars
7237
7446
  # # └─────┘
7238
7447
  def sample(
7239
7448
  fraction: nil,
7240
- with_replacement: nil,
7449
+ with_replacement: false,
7241
7450
  shuffle: false,
7242
7451
  seed: nil,
7243
7452
  n: nil
7244
7453
  )
7245
- # TODO update
7246
- if with_replacement.nil?
7247
- warn "The default `with_replacement` for `sample` method will change from `true` to `false` in a future version"
7248
- with_replacement = true
7249
- end
7250
-
7251
7454
  if !n.nil? && !fraction.nil?
7252
7455
  raise ArgumentError, "cannot specify both `n` and `fraction`"
7253
7456
  end
@@ -7636,13 +7839,7 @@ module Polars
7636
7839
  # # ╞═══════════╡
7637
7840
  # # │ -6.754888 │
7638
7841
  # # └───────────┘
7639
- def entropy(base: nil, normalize: true)
7640
- # TODO update (including param docs)
7641
- if base.nil?
7642
- warn "The default `base` for `entropy` method will change from `2` to `Math::E` in a future version"
7643
- base = 2
7644
- end
7645
-
7842
+ def entropy(base: Math::E, normalize: true)
7646
7843
  wrap_expr(_rbexpr.entropy(base, normalize))
7647
7844
  end
7648
7845
 
@@ -7698,6 +7895,8 @@ module Polars
7698
7895
  #
7699
7896
  # @param descending [Boolean]
7700
7897
  # Whether the `Series` order is descending.
7898
+ # @param nulls_last [Boolean]
7899
+ # Whether the nulls are at the end.
7701
7900
  #
7702
7901
  # @return [Expr]
7703
7902
  #
@@ -7717,12 +7916,16 @@ module Polars
7717
7916
  # # ╞════════╡
7718
7917
  # # │ 3 │
7719
7918
  # # └────────┘
7720
- def set_sorted(descending: false)
7721
- wrap_expr(_rbexpr.set_sorted_flag(descending))
7919
+ def set_sorted(descending: false, nulls_last: false)
7920
+ wrap_expr(_rbexpr.set_sorted_flag(descending, nulls_last))
7722
7921
  end
7723
7922
 
7724
7923
  # Aggregate to list.
7725
7924
  #
7925
+ # @param maintain_order [Boolean]
7926
+ # Whether to preserve the order of elements in the list. Setting this
7927
+ # to `false` can improve performance, especially within `group_by`.
7928
+ #
7726
7929
  # @return [Expr]
7727
7930
  #
7728
7931
  # @example
@@ -7742,8 +7945,8 @@ module Polars
7742
7945
  # # ╞═══════════╪═══════════╡
7743
7946
  # # │ [1, 2, 3] ┆ [4, 5, 6] │
7744
7947
  # # └───────────┴───────────┘
7745
- def implode
7746
- wrap_expr(_rbexpr.implode)
7948
+ def implode(maintain_order: true)
7949
+ wrap_expr(_rbexpr.implode(maintain_order))
7747
7950
  end
7748
7951
 
7749
7952
  # Bin values into buckets and count their occurrences.
@@ -81,13 +81,13 @@ module Polars
81
81
  )
82
82
  start_rbexpr = Utils.parse_into_expression(start)
83
83
  end_rbexpr = Utils.parse_into_expression(stop)
84
- unix_epoch = ::Date.new(1970, 1, 1)
84
+ holidays_rbexpr = Utils._holidays_to_expr(holidays)
85
85
  Utils.wrap_expr(
86
86
  Plr.business_day_count(
87
87
  start_rbexpr,
88
88
  end_rbexpr,
89
89
  week_mask,
90
- holidays.map { |holiday| holiday - unix_epoch }
90
+ holidays_rbexpr
91
91
  )
92
92
  )
93
93
  end
@@ -28,5 +28,35 @@ module Polars
28
28
  def self_dtype
29
29
  DataTypeExpr._from_rbdatatype_expr(RbDataTypeExpr.self_dtype)
30
30
  end
31
+
32
+ # Create a new datatype expression that represents a Struct datatype.
33
+ #
34
+ # @note
35
+ # This functionality is considered **unstable**. It may be changed
36
+ # at any point without it being considered a breaking change.
37
+ #
38
+ # @return [DataTypeExpr]
39
+ def struct_with_fields(
40
+ mapping
41
+ )
42
+ preprocess = lambda do |dtype_expr|
43
+ if dtype_expr.is_a?(DataType)
44
+ dtype_expr.to_dtype_expr._rbdatatype_expr
45
+ elsif dtype_expr < DataType
46
+ dtype_expr.to_dtype_expr._rbdatatype_expr
47
+ elsif dtype_expr.is_a?(DataTypeExpr)
48
+ dtype_expr._rbdatatype_expr
49
+ else
50
+ msg = "mapping item must be a datatype or datatype expression; found #{dtype_expr.inspect}"
51
+ raise TypeError, msg
52
+ end
53
+ end
54
+
55
+ fields = mapping.map { |name, dtype_expr| [name.to_s, preprocess.(dtype_expr)] }
56
+
57
+ DataTypeExpr._from_rbdatatype_expr(
58
+ RbDataTypeExpr.struct_with_fields(fields)
59
+ )
60
+ end
31
61
  end
32
62
  end
@@ -399,6 +399,85 @@ module Polars
399
399
  out
400
400
  end
401
401
 
402
+ # Merge multiple sorted DataFrames or LazyFrames by the sorted key.
403
+ #
404
+ # The output of this operation will also be sorted.
405
+ # It is the callers responsibility that the frames
406
+ # are sorted in ascending order by that key otherwise
407
+ # the output will not make sense.
408
+ #
409
+ # @note
410
+ # This functionality is considered **unstable**. It may be changed
411
+ # at any point without it being considered a breaking change.
412
+ #
413
+ # @param items [Array]
414
+ # DataFrames or LazyFrames to merge.
415
+ # @param key [String]
416
+ # Key that is sorted.
417
+ # @param maintain_order [Boolean]
418
+ # If `true`, the output is guaranteed to have left-biased ordering
419
+ # for equal keys: rows from the left frame appear before rows from
420
+ # the right frame when their keys are equal.
421
+ #
422
+ # @return [Object]
423
+ #
424
+ # @example
425
+ # df0 = Polars::DataFrame.new(
426
+ # {"name" => ["steve", "elise", "bob"], "age" => [42, 44, 18]}
427
+ # ).sort("age")
428
+ # df1 = Polars::DataFrame.new(
429
+ # {"name" => ["anna", "megan", "steve", "thomas"], "age" => [21, 33, 17, 20]}
430
+ # ).sort("age")
431
+ # df2 = Polars::DataFrame.new({"name" => ["ida", "maya"], "age" => [37, 27]}).sort("age")
432
+ # Polars.merge_sorted([df0, df1, df2], "age")
433
+ # # =>
434
+ # # shape: (9, 2)
435
+ # # ┌────────┬─────┐
436
+ # # │ name ┆ age │
437
+ # # │ --- ┆ --- │
438
+ # # │ str ┆ i64 │
439
+ # # ╞════════╪═════╡
440
+ # # │ steve ┆ 17 │
441
+ # # │ bob ┆ 18 │
442
+ # # │ thomas ┆ 20 │
443
+ # # │ anna ┆ 21 │
444
+ # # │ maya ┆ 27 │
445
+ # # │ megan ┆ 33 │
446
+ # # │ ida ┆ 37 │
447
+ # # │ steve ┆ 42 │
448
+ # # │ elise ┆ 44 │
449
+ # # └────────┴─────┘
450
+ def merge_sorted(
451
+ items,
452
+ key,
453
+ maintain_order: false
454
+ )
455
+ elems = items.to_a
456
+
457
+ if elems.empty?
458
+ msg = "cannot merge_sort empty list"
459
+ raise ArgumentError, msg
460
+ end
461
+ if elems.length == 1 && (elems[0].is_a?(DataFrame) || elems[0].is_a?(LazyFrame))
462
+ return elems[0]
463
+ end
464
+
465
+ if !Utils.is_non_empty_sequence_of(elems, DataFrame) && !Utils.is_non_empty_sequence_of(elems, LazyFrame)
466
+ msg = "merge_sorted is not supported for #{elems[0].inspect}"
467
+ raise TypeError, msg
468
+ end
469
+
470
+ frames = elems.map { |df| df.lazy }
471
+
472
+ reduce_fn = lambda do |x, y|
473
+ x.merge_sorted(y, key, maintain_order: maintain_order)
474
+ end
475
+
476
+ lf = Utils.reduce_balanced(reduce_fn, frames)
477
+ eager = elems[0].is_a?(DataFrame)
478
+ eager ? lf.collect : lf
479
+ end
480
+
402
481
  # Align an array of frames using the unique values from one or more columns as a key.
403
482
  #
404
483
  # Frames that do not contain the given key values have rows injected (with nulls
@@ -464,16 +543,10 @@ module Polars
464
543
  def align_frames(
465
544
  *frames,
466
545
  on:,
467
- how: nil,
546
+ how: "full",
468
547
  select: nil,
469
548
  descending: false
470
549
  )
471
- # TODO update
472
- if how.nil?
473
- warn "The default `how` for `align_frames` method will change from `left` to `full` in a future version"
474
- how = "left"
475
- end
476
-
477
550
  if frames.empty?
478
551
  return []
479
552
  elsif frames.map(&:class).uniq.length != 1