polars-df 0.15.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 (206) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +274 -0
  3. data/Cargo.lock +1465 -867
  4. data/Cargo.toml +3 -0
  5. data/LICENSE.txt +2 -2
  6. data/README.md +87 -37
  7. data/ext/polars/Cargo.toml +47 -16
  8. data/ext/polars/src/c_api/allocator.rs +7 -0
  9. data/ext/polars/src/c_api/mod.rs +1 -0
  10. data/ext/polars/src/catalog/mod.rs +1 -0
  11. data/ext/polars/src/catalog/unity.rs +470 -0
  12. data/ext/polars/src/conversion/any_value.rs +99 -84
  13. data/ext/polars/src/conversion/categorical.rs +30 -0
  14. data/ext/polars/src/conversion/chunked_array.rs +71 -62
  15. data/ext/polars/src/conversion/datetime.rs +63 -0
  16. data/ext/polars/src/conversion/mod.rs +796 -312
  17. data/ext/polars/src/dataframe/construction.rs +6 -18
  18. data/ext/polars/src/dataframe/export.rs +30 -39
  19. data/ext/polars/src/dataframe/general.rs +294 -362
  20. data/ext/polars/src/dataframe/io.rs +33 -150
  21. data/ext/polars/src/dataframe/map.rs +175 -0
  22. data/ext/polars/src/dataframe/mod.rs +37 -5
  23. data/ext/polars/src/dataframe/serde.rs +23 -8
  24. data/ext/polars/src/error.rs +44 -7
  25. data/ext/polars/src/exceptions.rs +21 -8
  26. data/ext/polars/src/expr/array.rs +86 -22
  27. data/ext/polars/src/expr/binary.rs +50 -1
  28. data/ext/polars/src/expr/bitwise.rs +39 -0
  29. data/ext/polars/src/expr/categorical.rs +20 -0
  30. data/ext/polars/src/expr/datatype.rs +51 -0
  31. data/ext/polars/src/expr/datetime.rs +99 -41
  32. data/ext/polars/src/expr/extension.rs +12 -0
  33. data/ext/polars/src/expr/general.rs +252 -128
  34. data/ext/polars/src/expr/list.rs +56 -60
  35. data/ext/polars/src/expr/meta.rs +30 -35
  36. data/ext/polars/src/expr/mod.rs +28 -6
  37. data/ext/polars/src/expr/name.rs +29 -14
  38. data/ext/polars/src/expr/rolling.rs +111 -3
  39. data/ext/polars/src/expr/selector.rs +219 -0
  40. data/ext/polars/src/expr/serde.rs +28 -0
  41. data/ext/polars/src/expr/string.rs +118 -20
  42. data/ext/polars/src/expr/struct.rs +14 -1
  43. data/ext/polars/src/file.rs +194 -86
  44. data/ext/polars/src/functions/aggregation.rs +13 -12
  45. data/ext/polars/src/functions/business.rs +2 -3
  46. data/ext/polars/src/functions/eager.rs +3 -2
  47. data/ext/polars/src/functions/io.rs +90 -18
  48. data/ext/polars/src/functions/lazy.rs +267 -118
  49. data/ext/polars/src/functions/meta.rs +8 -7
  50. data/ext/polars/src/functions/misc.rs +1 -1
  51. data/ext/polars/src/functions/mod.rs +2 -1
  52. data/ext/polars/src/functions/range.rs +88 -31
  53. data/ext/polars/src/functions/strings.rs +6 -0
  54. data/ext/polars/src/functions/utils.rs +8 -0
  55. data/ext/polars/src/interop/arrow/mod.rs +52 -1
  56. data/ext/polars/src/interop/arrow/{to_ruby.rs → to_rb.rs} +37 -7
  57. data/ext/polars/src/interop/arrow/to_rust.rs +43 -0
  58. data/ext/polars/src/interop/numo/to_numo_df.rs +1 -1
  59. data/ext/polars/src/interop/numo/to_numo_series.rs +72 -50
  60. data/ext/polars/src/io/cloud_options.rs +107 -0
  61. data/ext/polars/src/io/mod.rs +4 -0
  62. data/ext/polars/src/io/scan_options.rs +113 -0
  63. data/ext/polars/src/io/sink_options.rs +46 -0
  64. data/ext/polars/src/io/sink_output.rs +21 -0
  65. data/ext/polars/src/lazyframe/exitable.rs +39 -0
  66. data/ext/polars/src/lazyframe/general.rs +846 -368
  67. data/ext/polars/src/lazyframe/mod.rs +58 -5
  68. data/ext/polars/src/lazyframe/optflags.rs +59 -0
  69. data/ext/polars/src/lazyframe/serde.rs +36 -4
  70. data/ext/polars/src/lazyframe/sink.rs +46 -0
  71. data/ext/polars/src/lazygroupby.rs +38 -9
  72. data/ext/polars/src/lib.rs +574 -165
  73. data/ext/polars/src/map/lazy.rs +44 -74
  74. data/ext/polars/src/map/mod.rs +18 -254
  75. data/ext/polars/src/map/series.rs +241 -1087
  76. data/ext/polars/src/on_startup.rs +192 -9
  77. data/ext/polars/src/prelude.rs +1 -0
  78. data/ext/polars/src/rb_modules.rs +10 -57
  79. data/ext/polars/src/ruby/exceptions.rs +26 -0
  80. data/ext/polars/src/ruby/gvl.rs +104 -0
  81. data/ext/polars/src/ruby/lazy.rs +46 -0
  82. data/ext/polars/src/ruby/mod.rs +11 -0
  83. data/ext/polars/src/ruby/numo.rs +52 -0
  84. data/ext/polars/src/ruby/plan_callback.rs +198 -0
  85. data/ext/polars/src/ruby/rb_modules.rs +16 -0
  86. data/ext/polars/src/ruby/ruby_convert_registry.rs +51 -0
  87. data/ext/polars/src/ruby/ruby_function.rs +11 -0
  88. data/ext/polars/src/ruby/ruby_udf.rs +164 -0
  89. data/ext/polars/src/ruby/thread.rs +65 -0
  90. data/ext/polars/src/ruby/utils.rs +39 -0
  91. data/ext/polars/src/series/aggregation.rs +116 -91
  92. data/ext/polars/src/series/arithmetic.rs +16 -22
  93. data/ext/polars/src/series/comparison.rs +101 -222
  94. data/ext/polars/src/series/construction.rs +80 -70
  95. data/ext/polars/src/series/export.rs +98 -56
  96. data/ext/polars/src/series/general.rs +323 -440
  97. data/ext/polars/src/series/import.rs +22 -5
  98. data/ext/polars/src/series/map.rs +103 -0
  99. data/ext/polars/src/series/mod.rs +57 -15
  100. data/ext/polars/src/series/scatter.rs +139 -82
  101. data/ext/polars/src/sql.rs +16 -9
  102. data/ext/polars/src/testing/frame.rs +31 -0
  103. data/ext/polars/src/testing/mod.rs +5 -0
  104. data/ext/polars/src/testing/series.rs +31 -0
  105. data/ext/polars/src/timeout.rs +105 -0
  106. data/ext/polars/src/utils.rs +105 -4
  107. data/lib/polars/array_expr.rb +500 -22
  108. data/lib/polars/array_name_space.rb +384 -10
  109. data/lib/polars/batched_csv_reader.rb +48 -66
  110. data/lib/polars/binary_expr.rb +217 -0
  111. data/lib/polars/binary_name_space.rb +155 -1
  112. data/lib/polars/cat_expr.rb +224 -0
  113. data/lib/polars/cat_name_space.rb +132 -32
  114. data/lib/polars/catalog/unity/catalog_info.rb +20 -0
  115. data/lib/polars/catalog/unity/column_info.rb +31 -0
  116. data/lib/polars/catalog/unity/namespace_info.rb +21 -0
  117. data/lib/polars/catalog/unity/table_info.rb +50 -0
  118. data/lib/polars/catalog.rb +448 -0
  119. data/lib/polars/collect_batches.rb +22 -0
  120. data/lib/polars/config.rb +3 -3
  121. data/lib/polars/convert.rb +201 -36
  122. data/lib/polars/data_frame.rb +2851 -1017
  123. data/lib/polars/data_frame_plot.rb +173 -0
  124. data/lib/polars/data_type_expr.rb +52 -0
  125. data/lib/polars/data_type_group.rb +6 -0
  126. data/lib/polars/data_types.rb +118 -18
  127. data/lib/polars/date_time_expr.rb +426 -84
  128. data/lib/polars/date_time_name_space.rb +384 -111
  129. data/lib/polars/dynamic_group_by.rb +102 -10
  130. data/lib/polars/exceptions.rb +50 -5
  131. data/lib/polars/expr.rb +2159 -915
  132. data/lib/polars/extension_expr.rb +39 -0
  133. data/lib/polars/extension_name_space.rb +39 -0
  134. data/lib/polars/functions/aggregation/horizontal.rb +11 -6
  135. data/lib/polars/functions/aggregation/vertical.rb +2 -3
  136. data/lib/polars/functions/as_datatype.rb +290 -8
  137. data/lib/polars/functions/business.rb +95 -0
  138. data/lib/polars/functions/col.rb +6 -5
  139. data/lib/polars/functions/datatype.rb +62 -0
  140. data/lib/polars/functions/eager.rb +426 -24
  141. data/lib/polars/functions/escape_regex.rb +21 -0
  142. data/lib/polars/functions/lazy.rb +813 -195
  143. data/lib/polars/functions/lit.rb +21 -10
  144. data/lib/polars/functions/range/int_range.rb +74 -2
  145. data/lib/polars/functions/range/linear_space.rb +195 -0
  146. data/lib/polars/functions/range/time_range.rb +1 -1
  147. data/lib/polars/functions/repeat.rb +7 -12
  148. data/lib/polars/functions/whenthen.rb +2 -2
  149. data/lib/polars/group_by.rb +188 -58
  150. data/lib/polars/iceberg_dataset.rb +108 -0
  151. data/lib/polars/in_process_query.rb +37 -0
  152. data/lib/polars/io/cloud.rb +18 -0
  153. data/lib/polars/io/csv.rb +336 -128
  154. data/lib/polars/io/database.rb +19 -4
  155. data/lib/polars/io/delta.rb +134 -0
  156. data/lib/polars/io/iceberg.rb +34 -0
  157. data/lib/polars/io/ipc.rb +63 -63
  158. data/lib/polars/io/json.rb +16 -0
  159. data/lib/polars/io/lines.rb +172 -0
  160. data/lib/polars/io/ndjson.rb +176 -20
  161. data/lib/polars/io/parquet.rb +173 -95
  162. data/lib/polars/io/scan_options.rb +55 -0
  163. data/lib/polars/io/sink_options.rb +27 -0
  164. data/lib/polars/io/utils.rb +17 -0
  165. data/lib/polars/lazy_frame.rb +3017 -622
  166. data/lib/polars/lazy_group_by.rb +436 -2
  167. data/lib/polars/list_expr.rb +551 -59
  168. data/lib/polars/list_name_space.rb +465 -51
  169. data/lib/polars/meta_expr.rb +146 -24
  170. data/lib/polars/name_expr.rb +87 -2
  171. data/lib/polars/query_opt_flags.rb +264 -0
  172. data/lib/polars/rolling_group_by.rb +90 -5
  173. data/lib/polars/scan_cast_options.rb +86 -0
  174. data/lib/polars/schema.rb +128 -0
  175. data/lib/polars/selector.rb +245 -0
  176. data/lib/polars/selectors.rb +1048 -201
  177. data/lib/polars/series.rb +2522 -774
  178. data/lib/polars/series_plot.rb +72 -0
  179. data/lib/polars/slice.rb +1 -1
  180. data/lib/polars/sql_context.rb +13 -6
  181. data/lib/polars/string_cache.rb +19 -72
  182. data/lib/polars/string_expr.rb +561 -107
  183. data/lib/polars/string_name_space.rb +781 -109
  184. data/lib/polars/struct_expr.rb +139 -18
  185. data/lib/polars/struct_name_space.rb +19 -1
  186. data/lib/polars/testing.rb +24 -273
  187. data/lib/polars/utils/constants.rb +2 -0
  188. data/lib/polars/utils/construction/data_frame.rb +410 -0
  189. data/lib/polars/utils/construction/series.rb +350 -0
  190. data/lib/polars/utils/construction/utils.rb +9 -0
  191. data/lib/polars/utils/convert.rb +18 -8
  192. data/lib/polars/utils/deprecation.rb +11 -0
  193. data/lib/polars/utils/parse.rb +62 -9
  194. data/lib/polars/utils/reduce_balanced.rb +43 -0
  195. data/lib/polars/utils/serde.rb +22 -0
  196. data/lib/polars/utils/unstable.rb +19 -0
  197. data/lib/polars/utils/various.rb +86 -1
  198. data/lib/polars/utils.rb +63 -48
  199. data/lib/polars/version.rb +1 -1
  200. data/lib/polars.rb +85 -2
  201. metadata +80 -28
  202. data/ext/polars/src/allocator.rs +0 -13
  203. data/ext/polars/src/batched_csv.rs +0 -138
  204. data/ext/polars/src/functions/string_cache.rs +0 -25
  205. data/ext/polars/src/map/dataframe.rs +0 -338
  206. data/lib/polars/plot.rb +0 -109
@@ -1,8 +1,6 @@
1
1
  module Polars
2
2
  # Two-dimensional data structure representing data as a table with rows and columns.
3
3
  class DataFrame
4
- include Plot
5
-
6
4
  # @private
7
5
  attr_accessor :_df
8
6
 
@@ -15,11 +13,11 @@ module Polars
15
13
  # The schema of the resulting DataFrame. The schema may be declared in several
16
14
  # ways:
17
15
  #
18
- # * As a hash of name:type pairs; if type is nil, it will be auto-inferred.
16
+ # * As a hash of \\\\{name:type} pairs; if type is nil, it will be auto-inferred.
19
17
  # * As an array of column names; in this case types are automatically inferred.
20
- # * As an array of (name,type) pairs; this is equivalent to the dictionary form.
18
+ # * As an array of (name,type) pairs; this is equivalent to the hash form.
21
19
  #
22
- # If you supply a list of column names that does not match the names in the
20
+ # If you supply an array of column names that does not match the names in the
23
21
  # underlying data, the names given here will overwrite them. The number
24
22
  # of names given in the schema should match the underlying data dimensions.
25
23
  #
@@ -43,29 +41,40 @@ module Polars
43
41
  # @param infer_schema_length [Integer]
44
42
  # The maximum number of rows to scan for schema inference. If set to `nil`, the
45
43
  # full data may be scanned *(this can be slow)*. This parameter only applies if
46
- # the input data is a sequence or generator of rows; other input is read as-is.
44
+ # the input data is an array or generator of rows; other input is read as-is.
47
45
  # @param nan_to_null [Boolean]
48
46
  # If the data comes from one or more Numo arrays, can optionally convert input
49
47
  # data NaN values to null instead. This is a no-op for all other input data.
50
- def initialize(data = nil, schema: nil, columns: nil, schema_overrides: nil, strict: true, orient: nil, infer_schema_length: 100, nan_to_null: false)
51
- if schema && columns
52
- warn "columns is ignored when schema is passed"
53
- end
54
- schema ||= columns
55
-
48
+ def initialize(
49
+ data = nil,
50
+ schema: nil,
51
+ schema_overrides: nil,
52
+ strict: true,
53
+ orient: nil,
54
+ infer_schema_length: N_INFER_DEFAULT,
55
+ nan_to_null: false,
56
+ height: nil
57
+ )
56
58
  if defined?(ActiveRecord) && (data.is_a?(ActiveRecord::Relation) || data.is_a?(ActiveRecord::Result))
57
59
  raise ArgumentError, "Use read_database instead"
58
60
  end
59
61
 
62
+ if !height.nil?
63
+ msg = "the `height` parameter of `DataFrame` is considered unstable."
64
+ Utils.issue_unstable_warning(msg)
65
+
66
+ raise Todo
67
+ end
68
+
60
69
  if data.nil?
61
- self._df = self.class.hash_to_rbdf({}, schema: schema, schema_overrides: schema_overrides)
70
+ self._df = Utils.hash_to_rbdf({}, schema: schema, schema_overrides: schema_overrides)
62
71
  elsif data.is_a?(Hash)
63
72
  data = data.transform_keys { |v| v.is_a?(Symbol) ? v.to_s : v }
64
- self._df = self.class.hash_to_rbdf(data, schema: schema, schema_overrides: schema_overrides, strict: strict, nan_to_null: nan_to_null)
73
+ self._df = Utils.hash_to_rbdf(data, schema: schema, schema_overrides: schema_overrides, strict: strict, nan_to_null: nan_to_null)
65
74
  elsif data.is_a?(::Array)
66
- self._df = self.class.sequence_to_rbdf(data, schema: schema, schema_overrides: schema_overrides, strict: strict, orient: orient, infer_schema_length: infer_schema_length)
75
+ self._df = Utils.sequence_to_rbdf(data, schema: schema, schema_overrides: schema_overrides, strict: strict, orient: orient, infer_schema_length: infer_schema_length)
67
76
  elsif data.is_a?(Series)
68
- self._df = self.class.series_to_rbdf(data, schema: schema, schema_overrides: schema_overrides, strict: strict)
77
+ self._df = Utils.series_to_rbdf(data, schema: schema, schema_overrides: schema_overrides, strict: strict)
69
78
  elsif data.respond_to?(:arrow_c_stream)
70
79
  # This uses the fact that RbSeries.from_arrow_c_stream will create a
71
80
  # struct-typed Series. Then we unpack that to a DataFrame.
@@ -77,6 +86,43 @@ module Polars
77
86
  end
78
87
  end
79
88
 
89
+ # Read a serialized DataFrame from a file.
90
+ #
91
+ # @param source [Object]
92
+ # Path to a file or a file-like object (by file-like object, we refer to
93
+ # objects that have a `read` method, such as a file handler or `StringIO`).
94
+ #
95
+ # @return [DataFrame]
96
+ #
97
+ # @note
98
+ # Serialization is not stable across Polars versions: a LazyFrame serialized
99
+ # in one Polars version may not be deserializable in another Polars version.
100
+ #
101
+ # @example
102
+ # df = Polars::DataFrame.new({"a" => [1, 2, 3], "b" => [4.0, 5.0, 6.0]})
103
+ # bytes = df.serialize
104
+ # Polars::DataFrame.deserialize(StringIO.new(bytes))
105
+ # # =>
106
+ # # shape: (3, 2)
107
+ # # ┌─────┬─────┐
108
+ # # │ a ┆ b │
109
+ # # │ --- ┆ --- │
110
+ # # │ i64 ┆ f64 │
111
+ # # ╞═════╪═════╡
112
+ # # │ 1 ┆ 4.0 │
113
+ # # │ 2 ┆ 5.0 │
114
+ # # │ 3 ┆ 6.0 │
115
+ # # └─────┴─────┘
116
+ def self.deserialize(source)
117
+ if Utils.pathlike?(source)
118
+ source = Utils.normalize_filepath(source)
119
+ end
120
+
121
+ deserializer = RbDataFrame.method(:deserialize_binary)
122
+
123
+ _from_rbdf(deserializer.(source))
124
+ end
125
+
80
126
  # @private
81
127
  def self._from_rbdf(rb_df)
82
128
  df = DataFrame.allocate
@@ -84,6 +130,45 @@ module Polars
84
130
  df
85
131
  end
86
132
 
133
+ # Plot data.
134
+ #
135
+ # @return [Object]
136
+ def plot(x = nil, y = nil, type: nil, group: nil, stacked: nil)
137
+ plot = DataFramePlot.new(self)
138
+ return plot if x.nil? && y.nil?
139
+
140
+ raise ArgumentError, "Must specify columns" if x.nil? || y.nil?
141
+ type ||= begin
142
+ if self[x].dtype.numeric? && self[y].dtype.numeric?
143
+ "scatter"
144
+ elsif self[x].dtype == String && self[y].dtype.numeric?
145
+ "column"
146
+ elsif (self[x].dtype == Date || self[x].dtype == Datetime) && self[y].dtype.numeric?
147
+ "line"
148
+ else
149
+ raise "Cannot determine type. Use the type option."
150
+ end
151
+ end
152
+
153
+ case type
154
+ when "line"
155
+ plot.line(x, y, color: group)
156
+ when "area"
157
+ plot.area(x, y, color: group)
158
+ when "pie"
159
+ raise ArgumentError, "Cannot use group option with pie chart" unless group.nil?
160
+ plot.pie(x, y)
161
+ when "column"
162
+ plot.column(x, y, color: group, stacked: stacked)
163
+ when "bar"
164
+ plot.bar(x, y, color: group, stacked: stacked)
165
+ when "scatter"
166
+ plot.scatter(x, y, color: group)
167
+ else
168
+ raise ArgumentError, "Invalid type: #{type}"
169
+ end
170
+ end
171
+
87
172
  # Get the shape of the DataFrame.
88
173
  #
89
174
  # @return [Array]
@@ -212,9 +297,9 @@ module Polars
212
297
  # }
213
298
  # )
214
299
  # df.schema
215
- # # => {"foo"=>Polars::Int64, "bar"=>Polars::Float64, "ham"=>Polars::String}
300
+ # # => Polars::Schema({"foo"=>Polars::Int64, "bar"=>Polars::Float64, "ham"=>Polars::String})
216
301
  def schema
217
- columns.zip(dtypes).to_h
302
+ Schema.new(columns.zip(dtypes).to_h)
218
303
  end
219
304
 
220
305
  # Equal.
@@ -351,142 +436,243 @@ module Polars
351
436
  # Returns subset of the DataFrame.
352
437
  #
353
438
  # @return [Object]
354
- def [](*args)
355
- if args.size == 2
356
- row_selection, col_selection = args
357
-
358
- # df[.., unknown]
359
- if row_selection.is_a?(Range)
360
-
361
- # multiple slices
362
- # df[.., ..]
363
- if col_selection.is_a?(Range)
364
- raise Todo
365
- end
366
- end
367
-
368
- # df[2, ..] (select row as df)
369
- if row_selection.is_a?(Integer)
370
- if col_selection.is_a?(::Array)
371
- df = self[0.., col_selection]
372
- return df.slice(row_selection, 1)
373
- end
374
- # df[2, "a"]
375
- if col_selection.is_a?(::String) || col_selection.is_a?(Symbol)
376
- return self[col_selection][row_selection]
377
- end
378
- end
379
-
380
- # column selection can be "a" and ["a", "b"]
381
- if col_selection.is_a?(::String) || col_selection.is_a?(Symbol)
382
- col_selection = [col_selection]
383
- end
384
-
385
- # df[.., 1]
386
- if col_selection.is_a?(Integer)
387
- series = to_series(col_selection)
388
- return series[row_selection]
389
- end
390
-
391
- if col_selection.is_a?(::Array)
392
- # df[.., [1, 2]]
393
- if Utils.is_int_sequence(col_selection)
394
- series_list = col_selection.map { |i| to_series(i) }
395
- df = self.class.new(series_list)
396
- return df[row_selection]
397
- end
398
- end
399
-
400
- df = self[col_selection]
401
- return df[row_selection]
402
- elsif args.size == 1
403
- item = args[0]
404
-
405
- # select single column
406
- # df["foo"]
407
- if item.is_a?(::String) || item.is_a?(Symbol)
408
- return Utils.wrap_s(_df.get_column(item.to_s))
409
- end
410
-
411
- # df[idx]
412
- if item.is_a?(Integer)
413
- return slice(_pos_idx(item, 0), 1)
414
- end
415
-
416
- # df[..]
417
- if item.is_a?(Range)
418
- return Slice.new(self).apply(item)
419
- end
420
-
421
- if item.is_a?(::Array) && item.all? { |v| Utils.strlike?(v) }
422
- # select multiple columns
423
- # df[["foo", "bar"]]
424
- return _from_rbdf(_df.select(item.map(&:to_s)))
425
- end
426
-
427
- if Utils.is_int_sequence(item)
428
- item = Series.new("", item)
429
- end
430
-
431
- if item.is_a?(Series)
432
- dtype = item.dtype
433
- if dtype == String
434
- return _from_rbdf(_df.select(item))
435
- elsif dtype == UInt32
436
- return _from_rbdf(_df.take_with_series(item._s))
437
- elsif [UInt8, UInt16, UInt64, Int8, Int16, Int32, Int64].include?(dtype)
438
- return _from_rbdf(
439
- _df.take_with_series(_pos_idxs(item, 0)._s)
440
- )
441
- end
442
- end
443
- end
444
-
445
- # Ruby-specific
446
- if item.is_a?(Expr) || item.is_a?(Series)
447
- return filter(item)
448
- end
449
-
450
- raise ArgumentError, "Cannot get item of type: #{item.class.name}"
439
+ #
440
+ # @example
441
+ # df = Polars::DataFrame.new(
442
+ # {"a" => [1, 2, 3], "d" => [4, 5, 6], "c" => [1, 3, 2], "b" => [7, 8, 9]}
443
+ # )
444
+ # df[0]
445
+ # # =>
446
+ # # shape: (1, 4)
447
+ # # ┌─────┬─────┬─────┬─────┐
448
+ # # │ a ┆ d ┆ c ┆ b │
449
+ # # │ --- ┆ --- ┆ --- ┆ --- │
450
+ # # │ i64 ┆ i64 ┆ i64 ┆ i64 │
451
+ # # ╞═════╪═════╪═════╪═════╡
452
+ # # │ 1 ┆ 4 ┆ 1 ┆ 7 │
453
+ # # └─────┴─────┴─────┴─────┘
454
+ #
455
+ # @example
456
+ # df[0, "a"]
457
+ # # => 1
458
+ #
459
+ # @example
460
+ # df["a"]
461
+ # # =>
462
+ # # shape: (3,)
463
+ # # Series: 'a' [i64]
464
+ # # [
465
+ # # 1
466
+ # # 2
467
+ # # 3
468
+ # # ]
469
+ #
470
+ # @example
471
+ # df[0..1]
472
+ # # =>
473
+ # # shape: (2, 4)
474
+ # # ┌─────┬─────┬─────┬─────┐
475
+ # # │ a ┆ d ┆ c ┆ b │
476
+ # # │ --- ┆ --- ┆ --- ┆ --- │
477
+ # # i64 ┆ i64 ┆ i64 ┆ i64 │
478
+ # # ╞═════╪═════╪═════╪═════╡
479
+ # # 1 ┆ 4 ┆ 1 ┆ 7 │
480
+ # # 2 ┆ 5 ┆ 3 ┆ 8 │
481
+ # # └─────┴─────┴─────┴─────┘
482
+ #
483
+ # @example
484
+ # df[0..1, "a"]
485
+ # # =>
486
+ # # shape: (2,)
487
+ # # Series: 'a' [i64]
488
+ # # [
489
+ # # 1
490
+ # # 2
491
+ # # ]
492
+ #
493
+ # @example
494
+ # df[0..1, 0]
495
+ # # =>
496
+ # # shape: (2,)
497
+ # # Series: 'a' [i64]
498
+ # # [
499
+ # # 1
500
+ # # 2
501
+ # # ]
502
+ #
503
+ # @example
504
+ # df[[0, 1], [0, 1, 2]]
505
+ # # =>
506
+ # # shape: (2, 3)
507
+ # # ┌─────┬─────┬─────┐
508
+ # # a ┆ d ┆ c │
509
+ # # │ --- ┆ --- ┆ --- │
510
+ # # │ i64 ┆ i64 ┆ i64 │
511
+ # # ╞═════╪═════╪═════╡
512
+ # # │ 1 ┆ 4 ┆ 1 │
513
+ # # 2 ┆ 5 ┆ 3 │
514
+ # # └─────┴─────┴─────┘
515
+ #
516
+ # @example
517
+ # df[0..1, ["a", "c"]]
518
+ # # =>
519
+ # # shape: (2, 2)
520
+ # # ┌─────┬─────┐
521
+ # # │ a ┆ c │
522
+ # # --- ---
523
+ # # │ i64 ┆ i64 │
524
+ # # ╞═════╪═════╡
525
+ # # │ 1 ┆ 1 │
526
+ # # │ 2 ┆ 3 │
527
+ # # └─────┴─────┘
528
+ #
529
+ # @example
530
+ # df[0.., 0..1]
531
+ # # =>
532
+ # # shape: (3, 2)
533
+ # # ┌─────┬─────┐
534
+ # # │ a ┆ d │
535
+ # # --- ---
536
+ # # │ i64 ┆ i64 │
537
+ # # ╞═════╪═════╡
538
+ # # │ 1 ┆ 4 │
539
+ # # │ 2 ┆ 5 │
540
+ # # │ 3 ┆ 6 │
541
+ # # └─────┴─────┘
542
+ #
543
+ # @example
544
+ # df[0.., "a".."c"]
545
+ # # =>
546
+ # # shape: (3, 3)
547
+ # # ┌─────┬─────┬─────┐
548
+ # # │ a ┆ d ┆ c │
549
+ # # │ --- ┆ --- ┆ --- │
550
+ # # │ i64 ┆ i64 ┆ i64 │
551
+ # # ╞═════╪═════╪═════╡
552
+ # # │ 1 ┆ 4 ┆ 1 │
553
+ # # │ 2 ┆ 5 ┆ 3 │
554
+ # # │ 3 ┆ 6 ┆ 2 │
555
+ # # └─────┴─────┴─────┘
556
+ def [](*key)
557
+ get_df_item_by_key(self, key)
451
558
  end
452
559
 
453
560
  # Set item.
454
561
  #
455
562
  # @return [Object]
563
+ #
564
+ # @example `df[["a", "b"]] = value`:
565
+ # df = Polars::DataFrame.new({"a" => [1, 2, 3], "b" => [4, 5, 6]})
566
+ # df[["a", "b"]] = [[10, 40], [20, 50], [30, 60]]
567
+ # df
568
+ # # =>
569
+ # # shape: (3, 2)
570
+ # # ┌─────┬─────┐
571
+ # # │ a ┆ b │
572
+ # # │ --- ┆ --- │
573
+ # # │ i64 ┆ i64 │
574
+ # # ╞═════╪═════╡
575
+ # # │ 10 ┆ 40 │
576
+ # # │ 20 ┆ 50 │
577
+ # # │ 30 ┆ 60 │
578
+ # # └─────┴─────┘
579
+ #
580
+ # @example `df[row_idx, "a"] = value`:
581
+ # df[1, "a"] = 100
582
+ # df
583
+ # # =>
584
+ # # shape: (3, 2)
585
+ # # ┌─────┬─────┐
586
+ # # │ a ┆ b │
587
+ # # │ --- ┆ --- │
588
+ # # │ i64 ┆ i64 │
589
+ # # ╞═════╪═════╡
590
+ # # │ 10 ┆ 40 │
591
+ # # │ 100 ┆ 50 │
592
+ # # │ 30 ┆ 60 │
593
+ # # └─────┴─────┘
594
+ #
595
+ # @example `df[row_idx, col_idx] = value`:
596
+ # df[0, 1] = 30
597
+ # df
598
+ # # =>
599
+ # # shape: (3, 2)
600
+ # # ┌─────┬─────┐
601
+ # # │ a ┆ b │
602
+ # # │ --- ┆ --- │
603
+ # # │ i64 ┆ i64 │
604
+ # # ╞═════╪═════╡
605
+ # # │ 10 ┆ 30 │
606
+ # # │ 100 ┆ 50 │
607
+ # # │ 30 ┆ 60 │
608
+ # # └─────┴─────┘
456
609
  def []=(*key, value)
457
- if key.length == 1
458
- key = key.first
459
- elsif key.length != 2
610
+ if key.empty? || key.length > 2
460
611
  raise ArgumentError, "wrong number of arguments (given #{key.length + 1}, expected 2..3)"
461
612
  end
462
613
 
463
- if Utils.strlike?(key)
614
+ if key.length == 1 && Utils.strlike?(key[0])
615
+ key = key[0]
616
+
464
617
  if value.is_a?(::Array) || (defined?(Numo::NArray) && value.is_a?(Numo::NArray))
465
618
  value = Series.new(value)
466
619
  elsif !value.is_a?(Series)
467
620
  value = Polars.lit(value)
468
621
  end
469
- self._df = with_column(value.alias(key.to_s))._df
470
- elsif key.is_a?(::Array)
622
+ self._df = with_columns(value.alias(key.to_s))._df
623
+
624
+ # df[["C", "D"]]
625
+ elsif key.length == 1 && key[0].is_a?(::Array)
626
+ key = key[0]
627
+
628
+ if !value.is_a?(::Array) || !value.all? { |v| v.is_a?(::Array) }
629
+ msg = "can only set multiple columns with 2D matrix"
630
+ raise ArgumentError, msg
631
+ end
632
+ if value.any? { |v| v.size != key.length }
633
+ msg = "matrix columns should be equal to list used to determine column names"
634
+ raise ArgumentError, msg
635
+ end
636
+
637
+ columns = []
638
+ key.each_with_index do |name, i|
639
+ columns << Series.new(name, value.map { |v| v[i] })
640
+ end
641
+ self._df = with_columns(columns)._df
642
+
643
+ # df[a, b]
644
+ else
471
645
  row_selection, col_selection = key
472
646
 
647
+ if (row_selection.is_a?(Series) && row_selection.dtype == Boolean) || Utils.is_bool_sequence(row_selection)
648
+ msg = (
649
+ "not allowed to set DataFrame by boolean mask in the row position" +
650
+ "\n\nConsider using `DataFrame.with_columns`."
651
+ )
652
+ raise TypeError, msg
653
+ end
654
+
655
+ # get series column selection
473
656
  if Utils.strlike?(col_selection)
474
657
  s = self[col_selection]
475
658
  elsif col_selection.is_a?(Integer)
476
- raise Todo
659
+ s = self[0.., col_selection]
477
660
  else
478
- raise ArgumentError, "column selection not understood: #{col_selection}"
661
+ msg = "unexpected column selection #{col_selection.inspect}"
662
+ raise TypeError, msg
479
663
  end
480
664
 
665
+ # dispatch to []= of Series to do modification
481
666
  s[row_selection] = value
482
667
 
668
+ # now find the location to place series
669
+ # df[idx]
483
670
  if col_selection.is_a?(Integer)
484
671
  replace_column(col_selection, s)
672
+ # df["foo"]
485
673
  elsif Utils.strlike?(col_selection)
486
- replace(col_selection, s)
674
+ _replace(col_selection.to_s, s)
487
675
  end
488
- else
489
- raise Todo
490
676
  end
491
677
  end
492
678
 
@@ -495,22 +681,94 @@ module Polars
495
681
  _df.arrow_c_stream
496
682
  end
497
683
 
498
- # Return the dataframe as a scalar.
684
+ # Get an ordered mapping of column names to their data type.
685
+ #
686
+ # @return [Schema]
687
+ #
688
+ # @note
689
+ # This method is included to facilitate writing code that is generic for both
690
+ # DataFrame and LazyFrame.
691
+ #
692
+ # @example Determine the schema.
693
+ # df = Polars::DataFrame.new(
694
+ # {
695
+ # "foo" => [1, 2, 3],
696
+ # "bar" => [6.0, 7.0, 8.0],
697
+ # "ham" => ["a", "b", "c"]
698
+ # }
699
+ # )
700
+ # df.collect_schema
701
+ # # => Polars::Schema({"foo"=>Polars::Int64, "bar"=>Polars::Float64, "ham"=>Polars::String})
702
+ #
703
+ # @example Access various properties of the schema using the `Schema` object.
704
+ # schema = df.collect_schema
705
+ # schema["bar"]
706
+ # # => Polars::Float64
707
+ #
708
+ # @example
709
+ # schema.names
710
+ # # => ["foo", "bar", "ham"]
711
+ #
712
+ # @example
713
+ # schema.dtypes
714
+ # # => [Polars::Int64, Polars::Float64, Polars::String]
715
+ #
716
+ # @example
717
+ # schema.length
718
+ # # => 3
719
+ def collect_schema
720
+ Schema.new(columns.zip(dtypes), check_dtypes: false)
721
+ end
722
+
723
+ # Return the DataFrame as a scalar, or return the element at the given row/column.
499
724
  #
500
- # Equivalent to `df[0,0]`, with a check that the shape is (1,1).
725
+ # @param row [Integer]
726
+ # Optional row index.
727
+ # @param column [Integer, String]
728
+ # Optional column index or name.
501
729
  #
502
730
  # @return [Object]
503
731
  #
732
+ # @note
733
+ # If row/col not provided, this is equivalent to `df[0,0]`, with a check that
734
+ # the shape is (1,1). With row/col, this is equivalent to `df[row,col]`.
735
+ #
504
736
  # @example
505
737
  # df = Polars::DataFrame.new({"a" => [1, 2, 3], "b" => [4, 5, 6]})
506
- # result = df.select((Polars.col("a") * Polars.col("b")).sum)
507
- # result.item
738
+ # df.select((Polars.col("a") * Polars.col("b")).sum).item
508
739
  # # => 32
509
- def item
510
- if shape != [1, 1]
511
- raise ArgumentError, "Can only call .item if the dataframe is of shape (1,1), dataframe is of shape #{shape}"
740
+ #
741
+ # @example
742
+ # df.item(1, 1)
743
+ # # => 5
744
+ #
745
+ # @example
746
+ # df.item(2, "b")
747
+ # # => 6
748
+ def item(row = nil, column = nil)
749
+ if row.nil? && column.nil?
750
+ if shape != [1, 1]
751
+ msg = (
752
+ "can only call `.item()` if the dataframe is of shape (1, 1)," +
753
+ " or if explicit row/col values are provided;" +
754
+ " frame has shape #{shape.inspect}"
755
+ )
756
+ raise ArgumentError, msg
757
+ end
758
+ return _df.to_series(0).get_index(0)
759
+
760
+ elsif row.nil? || column.nil?
761
+ msg = "cannot call `.item()` with only one of `row` or `column`"
762
+ raise ArgumentError, msg
512
763
  end
513
- self[0, 0]
764
+
765
+ s =
766
+ if column.is_a?(Integer)
767
+ _df.to_series(column)
768
+ else
769
+ _df.get_column(column)
770
+ end
771
+ s.get_index_signed(row)
514
772
  end
515
773
 
516
774
  # no to_arrow
@@ -526,9 +784,7 @@ module Polars
526
784
  end
527
785
  end
528
786
 
529
- # Convert every row to a dictionary.
530
- #
531
- # Note that this is slow.
787
+ # Convert every row to a hash.
532
788
  #
533
789
  # @return [Array]
534
790
  #
@@ -538,13 +794,8 @@ module Polars
538
794
  # # =>
539
795
  # # [{"foo"=>1, "bar"=>4}, {"foo"=>2, "bar"=>5}, {"foo"=>3, "bar"=>6}]
540
796
  def to_hashes
541
- rbdf = _df
542
- names = columns
543
-
544
- height.times.map do |i|
545
- names.zip(rbdf.row_tuple(i)).to_h
546
- end
547
- end
797
+ rows(named: true)
798
+ end
548
799
 
549
800
  # Convert DataFrame to a 2D Numo array.
550
801
  #
@@ -597,17 +848,51 @@ module Polars
597
848
  if index < 0
598
849
  index = columns.length + index
599
850
  end
600
- Utils.wrap_s(_df.select_at_idx(index))
851
+ Utils.wrap_s(_df.to_series(index))
852
+ end
853
+
854
+ # Serialize this DataFrame to a file or string.
855
+ #
856
+ # @param file [Object]
857
+ # File path or writable file-like object to which the result will be written.
858
+ # If set to `nil` (default), the output is returned as a string instead.
859
+ #
860
+ # @return [Object]
861
+ #
862
+ # @note
863
+ # Serialization is not stable across Polars versions: a LazyFrame serialized
864
+ # in one Polars version may not be deserializable in another Polars version.
865
+ #
866
+ # @example
867
+ # df = Polars::DataFrame.new(
868
+ # {
869
+ # "foo" => [1, 2, 3],
870
+ # "bar" => [6, 7, 8]
871
+ # }
872
+ # )
873
+ # bytes = df.serialize
874
+ # Polars::DataFrame.deserialize(StringIO.new(bytes))
875
+ # # =>
876
+ # # shape: (3, 2)
877
+ # # ┌─────┬─────┐
878
+ # # │ foo ┆ bar │
879
+ # # │ --- ┆ --- │
880
+ # # │ i64 ┆ i64 │
881
+ # # ╞═════╪═════╡
882
+ # # │ 1 ┆ 6 │
883
+ # # │ 2 ┆ 7 │
884
+ # # │ 3 ┆ 8 │
885
+ # # └─────┴─────┘
886
+ def serialize(file = nil)
887
+ serializer = _df.method(:serialize_binary)
888
+
889
+ Utils.serialize_polars_object(serializer, file)
601
890
  end
602
891
 
603
892
  # Serialize to JSON representation.
604
893
  #
605
894
  # @param file [String]
606
895
  # File path to which the result should be written.
607
- # @param pretty [Boolean]
608
- # Pretty serialize json.
609
- # @param row_oriented [Boolean]
610
- # Write to row oriented json. This is slower, but more common.
611
896
  #
612
897
  # @return [nil]
613
898
  #
@@ -619,16 +904,8 @@ module Polars
619
904
  # }
620
905
  # )
621
906
  # df.write_json
622
- # # => "{\"columns\":[{\"name\":\"foo\",\"datatype\":\"Int64\",\"bit_settings\":\"\",\"values\":[1,2,3]},{\"name\":\"bar\",\"datatype\":\"Int64\",\"bit_settings\":\"\",\"values\":[6,7,8]}]}"
623
- #
624
- # @example
625
- # df.write_json(row_oriented: true)
626
907
  # # => "[{\"foo\":1,\"bar\":6},{\"foo\":2,\"bar\":7},{\"foo\":3,\"bar\":8}]"
627
- def write_json(
628
- file = nil,
629
- pretty: false,
630
- row_oriented: false
631
- )
908
+ def write_json(file = nil)
632
909
  if Utils.pathlike?(file)
633
910
  file = Utils.normalize_filepath(file)
634
911
  end
@@ -636,7 +913,7 @@ module Polars
636
913
  if file.nil? || to_string_io
637
914
  buf = StringIO.new
638
915
  buf.set_encoding(Encoding::BINARY)
639
- _df.write_json(buf, pretty, row_oriented)
916
+ _df.write_json(buf)
640
917
  json_bytes = buf.string
641
918
 
642
919
  json_str = json_bytes.force_encoding(Encoding::UTF_8)
@@ -646,7 +923,7 @@ module Polars
646
923
  return json_str
647
924
  end
648
925
  else
649
- _df.write_json(file, pretty, row_oriented)
926
+ _df.write_json(file)
650
927
  end
651
928
  nil
652
929
  end
@@ -667,26 +944,38 @@ module Polars
667
944
  # )
668
945
  # df.write_ndjson
669
946
  # # => "{\"foo\":1,\"bar\":6}\n{\"foo\":2,\"bar\":7}\n{\"foo\":3,\"bar\":8}\n"
670
- def write_ndjson(file = nil)
671
- if Utils.pathlike?(file)
672
- file = Utils.normalize_filepath(file)
947
+ def write_ndjson(
948
+ file = nil,
949
+ compression: "uncompressed",
950
+ compression_level: nil,
951
+ check_extension: true
952
+ )
953
+ should_return_buffer = false
954
+ if file.nil?
955
+ target = StringIO.new
956
+ target.set_encoding(Encoding::BINARY)
957
+ should_return_buffer = true
958
+ elsif Utils.pathlike?(file)
959
+ target = Utils.normalize_filepath(file)
960
+ else
961
+ target = file
673
962
  end
674
- to_string_io = !file.nil? && file.is_a?(StringIO)
675
- if file.nil? || to_string_io
676
- buf = StringIO.new
677
- buf.set_encoding(Encoding::BINARY)
678
- _df.write_ndjson(buf)
679
- json_bytes = buf.string
680
963
 
681
- json_str = json_bytes.force_encoding(Encoding::UTF_8)
682
- if to_string_io
683
- file.write(json_str)
684
- else
685
- return json_str
686
- end
687
- else
688
- _df.write_ndjson(file)
964
+ engine = "in-memory"
965
+
966
+ lazy.sink_ndjson(
967
+ target,
968
+ compression: compression,
969
+ compression_level: compression_level,
970
+ check_extension: check_extension,
971
+ optimizations: QueryOptFlags._eager,
972
+ engine: engine
973
+ )
974
+
975
+ if should_return_buffer
976
+ return target.string.force_encoding(Encoding::UTF_8)
689
977
  end
978
+
690
979
  nil
691
980
  end
692
981
 
@@ -695,11 +984,11 @@ module Polars
695
984
  # @param file [String, nil]
696
985
  # File path to which the result should be written. If set to `nil`
697
986
  # (default), the output is returned as a string instead.
698
- # @param has_header [Boolean]
987
+ # @param include_header [Boolean]
699
988
  # Whether to include header in the CSV output.
700
- # @param sep [String]
989
+ # @param separator [String]
701
990
  # Separate CSV fields with this symbol.
702
- # @param quote [String]
991
+ # @param quote_char [String]
703
992
  # Byte to use as quoting character.
704
993
  # @param batch_size [Integer]
705
994
  # Number of rows that will be processed per thread.
@@ -718,8 +1007,8 @@ module Polars
718
1007
  # [chrono](https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
719
1008
  # Rust crate.
720
1009
  # @param float_precision [Integer, nil]
721
- # Number of decimal places to write, applied to both `:f32` and
722
- # `:f64` datatypes.
1010
+ # Number of decimal places to write, applied to both `Float32` and
1011
+ # `Float64` datatypes.
723
1012
  # @param null_value [String, nil]
724
1013
  # A string representing null values (defaulting to the empty string).
725
1014
  #
@@ -736,61 +1025,71 @@ module Polars
736
1025
  # df.write_csv("file.csv")
737
1026
  def write_csv(
738
1027
  file = nil,
739
- has_header: true,
740
- include_header: nil,
741
- sep: ",",
742
- quote: '"',
1028
+ include_bom: false,
1029
+ compression: "uncompressed",
1030
+ compression_level: nil,
1031
+ check_extension: true,
1032
+ include_header: true,
1033
+ separator: ",",
1034
+ line_terminator: "\n",
1035
+ quote_char: '"',
743
1036
  batch_size: 1024,
744
1037
  datetime_format: nil,
745
1038
  date_format: nil,
746
1039
  time_format: nil,
1040
+ float_scientific: nil,
747
1041
  float_precision: nil,
748
- null_value: nil
1042
+ decimal_comma: false,
1043
+ null_value: nil,
1044
+ quote_style: nil,
1045
+ storage_options: nil,
1046
+ credential_provider: "auto",
1047
+ retries: nil
749
1048
  )
750
- include_header = has_header if include_header.nil?
751
-
752
- if sep.length > 1
753
- raise ArgumentError, "only single byte separator is allowed"
754
- elsif quote.length > 1
755
- raise ArgumentError, "only single byte quote char is allowed"
756
- elsif null_value == ""
1049
+ Utils._check_arg_is_1byte("separator", separator, false)
1050
+ Utils._check_arg_is_1byte("quote_char", quote_char, true)
1051
+ if null_value == ""
757
1052
  null_value = nil
758
1053
  end
759
1054
 
1055
+ should_return_buffer = false
760
1056
  if file.nil?
761
- buffer = StringIO.new
762
- buffer.set_encoding(Encoding::BINARY)
763
- _df.write_csv(
764
- buffer,
765
- include_header,
766
- sep.ord,
767
- quote.ord,
768
- batch_size,
769
- datetime_format,
770
- date_format,
771
- time_format,
772
- float_precision,
773
- null_value
774
- )
775
- return buffer.string.force_encoding(Encoding::UTF_8)
1057
+ target = StringIO.new
1058
+ target.set_encoding(Encoding::BINARY)
1059
+ should_return_buffer = true
1060
+ else
1061
+ target = file
776
1062
  end
777
1063
 
778
- if Utils.pathlike?(file)
779
- file = Utils.normalize_filepath(file)
1064
+ engine = "in-memory"
1065
+
1066
+ lazy.sink_csv(
1067
+ target,
1068
+ include_bom: include_bom,
1069
+ include_header: include_header,
1070
+ separator: separator,
1071
+ line_terminator: line_terminator,
1072
+ quote_char: quote_char,
1073
+ batch_size: batch_size,
1074
+ datetime_format: datetime_format,
1075
+ date_format: date_format,
1076
+ time_format: time_format,
1077
+ float_scientific: float_scientific,
1078
+ float_precision: float_precision,
1079
+ decimal_comma: decimal_comma,
1080
+ null_value: null_value,
1081
+ quote_style: quote_style,
1082
+ storage_options: storage_options,
1083
+ credential_provider: credential_provider,
1084
+ retries: retries,
1085
+ optimizations: QueryOptFlags._eager,
1086
+ engine: engine
1087
+ )
1088
+
1089
+ if should_return_buffer
1090
+ return target.string.force_encoding(Encoding::UTF_8)
780
1091
  end
781
1092
 
782
- _df.write_csv(
783
- file,
784
- include_header,
785
- sep.ord,
786
- quote.ord,
787
- batch_size,
788
- datetime_format,
789
- date_format,
790
- time_format,
791
- float_precision,
792
- null_value,
793
- )
794
1093
  nil
795
1094
  end
796
1095
 
@@ -807,6 +1106,8 @@ module Polars
807
1106
  # File path to which the file should be written.
808
1107
  # @param compression ["uncompressed", "snappy", "deflate"]
809
1108
  # Compression method. Defaults to "uncompressed".
1109
+ # @param name [String]
1110
+ # Schema name. Defaults to empty string.
810
1111
  #
811
1112
  # @return [nil]
812
1113
  def write_avro(file, compression = "uncompressed", name: "")
@@ -829,28 +1130,59 @@ module Polars
829
1130
  # File path to which the file should be written.
830
1131
  # @param compression ["uncompressed", "lz4", "zstd"]
831
1132
  # Compression method. Defaults to "uncompressed".
1133
+ # @param compat_level [Object]
1134
+ # Use a specific compatibility level
1135
+ # when exporting Polars' internal data structures.
1136
+ # @param storage_options [Hash]
1137
+ # Options that indicate how to connect to a cloud provider.
1138
+ #
1139
+ # The cloud providers currently supported are AWS, GCP, and Azure.
1140
+ # See supported keys here:
1141
+ #
1142
+ # * [aws](https://docs.rs/object_store/latest/object_store/aws/enum.AmazonS3ConfigKey.html)
1143
+ # * [gcp](https://docs.rs/object_store/latest/object_store/gcp/enum.GoogleConfigKey.html)
1144
+ # * [azure](https://docs.rs/object_store/latest/object_store/azure/enum.AzureConfigKey.html)
1145
+ # * Hugging Face (`hf://`): Accepts an API key under the `token` parameter: `{'token': '...'}`, or by setting the `HF_TOKEN` environment variable.
1146
+ #
1147
+ # If `storage_options` is not provided, Polars will try to infer the
1148
+ # information from environment variables.
1149
+ # @param credential_provider [Object]
1150
+ # Provide a function that can be called to provide cloud storage
1151
+ # credentials. The function is expected to return a hash of
1152
+ # credential keys along with an optional credential expiry time.
1153
+ # @param retries [Integer]
1154
+ # Number of retries if accessing a cloud instance fails.
832
1155
  #
833
1156
  # @return [nil]
834
- def write_ipc(file, compression: "uncompressed", compat_level: nil)
1157
+ def write_ipc(
1158
+ file,
1159
+ compression: "uncompressed",
1160
+ compat_level: nil,
1161
+ record_batch_size: nil,
1162
+ storage_options: nil,
1163
+ credential_provider: "auto",
1164
+ retries: nil
1165
+ )
835
1166
  return_bytes = file.nil?
836
- if return_bytes
837
- file = StringIO.new
838
- file.set_encoding(Encoding::BINARY)
839
- end
840
- if Utils.pathlike?(file)
841
- file = Utils.normalize_filepath(file)
842
- end
843
-
844
- if compat_level.nil?
845
- compat_level = true
846
- end
847
-
848
- if compression.nil?
849
- compression = "uncompressed"
1167
+ if file.nil?
1168
+ target = StringIO.new
1169
+ target.set_encoding(Encoding::BINARY)
1170
+ else
1171
+ target = file
850
1172
  end
851
1173
 
852
- _df.write_ipc(file, compression, compat_level)
853
- return_bytes ? file.string : nil
1174
+ lazy.sink_ipc(
1175
+ target,
1176
+ compression: compression,
1177
+ compat_level: compat_level,
1178
+ record_batch_size: record_batch_size,
1179
+ storage_options: storage_options,
1180
+ credential_provider: credential_provider,
1181
+ retries: retries,
1182
+ optimizations: QueryOptFlags._eager,
1183
+ engine: "streaming"
1184
+ )
1185
+ return_bytes ? target.string : nil
854
1186
  end
855
1187
 
856
1188
  # Write to Arrow IPC record batch stream.
@@ -859,9 +1191,12 @@ module Polars
859
1191
  #
860
1192
  # @param file [Object]
861
1193
  # Path or writable file-like object to which the IPC record batch data will
862
- # be written. If set to `None`, the output is returned as a BytesIO object.
1194
+ # be written. If set to `nil`, the output is returned as a BytesIO object.
863
1195
  # @param compression ['uncompressed', 'lz4', 'zstd']
864
1196
  # Compression method. Defaults to "uncompressed".
1197
+ # @param compat_level [Object]
1198
+ # Use a specific compatibility level
1199
+ # when exporting Polars' internal data structures.
865
1200
  #
866
1201
  # @return [Object]
867
1202
  #
@@ -927,9 +1262,17 @@ module Polars
927
1262
  file,
928
1263
  compression: "zstd",
929
1264
  compression_level: nil,
930
- statistics: false,
1265
+ statistics: true,
931
1266
  row_group_size: nil,
932
- data_page_size: nil
1267
+ data_page_size: nil,
1268
+ partition_by: nil,
1269
+ partition_chunk_size_bytes: 4_294_967_296,
1270
+ storage_options: nil,
1271
+ credential_provider: "auto",
1272
+ retries: nil,
1273
+ metadata: nil,
1274
+ arrow_schema: nil,
1275
+ mkdir: false
933
1276
  )
934
1277
  if compression.nil?
935
1278
  compression = "uncompressed"
@@ -938,29 +1281,252 @@ module Polars
938
1281
  file = Utils.normalize_filepath(file)
939
1282
  end
940
1283
 
941
- if statistics == true
942
- statistics = {
943
- min: true,
944
- max: true,
945
- distinct_count: false,
946
- null_count: true
947
- }
948
- elsif statistics == false
949
- statistics = {}
950
- elsif statistics == "full"
951
- statistics = {
952
- min: true,
953
- max: true,
954
- distinct_count: true,
955
- null_count: true
956
- }
1284
+ target = file
1285
+ engine = "streaming"
1286
+ if !partition_by.nil?
1287
+ raise Todo
957
1288
  end
958
1289
 
959
- _df.write_parquet(
960
- file, compression, compression_level, statistics, row_group_size, data_page_size
1290
+ lazy.sink_parquet(
1291
+ target,
1292
+ compression: compression,
1293
+ compression_level: compression_level,
1294
+ statistics: statistics,
1295
+ row_group_size: row_group_size,
1296
+ data_page_size: data_page_size,
1297
+ storage_options: storage_options,
1298
+ credential_provider: credential_provider,
1299
+ retries: retries,
1300
+ metadata: metadata,
1301
+ arrow_schema: arrow_schema,
1302
+ engine: engine,
1303
+ mkdir: mkdir,
1304
+ optimizations: QueryOptFlags._eager
961
1305
  )
962
1306
  end
963
1307
 
1308
+ # Write the data in a Polars DataFrame to a database.
1309
+ #
1310
+ # @param table_name [String]
1311
+ # Schema-qualified name of the table to create or append to in the target
1312
+ # SQL database.
1313
+ # @param connection [Object]
1314
+ # An existing Active Record connection against the target database.
1315
+ # @param if_table_exists ['append', 'replace', 'fail']
1316
+ # The insert mode:
1317
+ #
1318
+ # * 'replace' will create a new database table, overwriting an existing one.
1319
+ # * 'append' will append to an existing table.
1320
+ # * 'fail' will fail if table already exists.
1321
+ #
1322
+ # @return [Integer]
1323
+ #
1324
+ # @note
1325
+ # This functionality is experimental. It may be changed at any point without it being considered a breaking change.
1326
+ def write_database(table_name, connection = nil, if_table_exists: "fail")
1327
+ if !defined?(ActiveRecord)
1328
+ raise Error, "Active Record not available"
1329
+ elsif ActiveRecord::VERSION::MAJOR < 7
1330
+ raise Error, "Requires Active Record 7+"
1331
+ end
1332
+
1333
+ valid_write_modes = ["append", "replace", "fail"]
1334
+ if !valid_write_modes.include?(if_table_exists)
1335
+ msg = "write_database `if_table_exists` must be one of #{valid_write_modes.inspect}, got #{if_table_exists.inspect}"
1336
+ raise ArgumentError, msg
1337
+ end
1338
+
1339
+ with_connection(connection) do |connection|
1340
+ table_exists = connection.table_exists?(table_name)
1341
+ if table_exists && if_table_exists == "fail"
1342
+ raise ArgumentError, "Table already exists"
1343
+ end
1344
+
1345
+ create_table = !table_exists || if_table_exists == "replace"
1346
+ maybe_transaction(connection, create_table) do
1347
+ if create_table
1348
+ mysql = connection.adapter_name.match?(/mysql|trilogy/i)
1349
+ force = if_table_exists == "replace"
1350
+ connection.create_table(table_name, id: false, force: force) do |t|
1351
+ schema.each do |c, dtype|
1352
+ options = {}
1353
+ column_type =
1354
+ case dtype
1355
+ when Binary
1356
+ :binary
1357
+ when Boolean
1358
+ :boolean
1359
+ when Date
1360
+ :date
1361
+ when Datetime
1362
+ :datetime
1363
+ when Decimal
1364
+ if mysql
1365
+ options[:precision] = dtype.precision || 65
1366
+ options[:scale] = dtype.scale || 30
1367
+ end
1368
+ :decimal
1369
+ when Float32
1370
+ options[:limit] = 24
1371
+ :float
1372
+ when Float64
1373
+ options[:limit] = 53
1374
+ :float
1375
+ when Int8
1376
+ options[:limit] = 1
1377
+ :integer
1378
+ when Int16
1379
+ options[:limit] = 2
1380
+ :integer
1381
+ when Int32
1382
+ options[:limit] = 4
1383
+ :integer
1384
+ when Int64
1385
+ options[:limit] = 8
1386
+ :integer
1387
+ when UInt8
1388
+ if mysql
1389
+ options[:limit] = 1
1390
+ options[:unsigned] = true
1391
+ else
1392
+ options[:limit] = 2
1393
+ end
1394
+ :integer
1395
+ when UInt16
1396
+ if mysql
1397
+ options[:limit] = 2
1398
+ options[:unsigned] = true
1399
+ else
1400
+ options[:limit] = 4
1401
+ end
1402
+ :integer
1403
+ when UInt32
1404
+ if mysql
1405
+ options[:limit] = 4
1406
+ options[:unsigned] = true
1407
+ else
1408
+ options[:limit] = 8
1409
+ end
1410
+ :integer
1411
+ when UInt64
1412
+ if mysql
1413
+ options[:limit] = 8
1414
+ options[:unsigned] = true
1415
+ :integer
1416
+ else
1417
+ options[:precision] = 20
1418
+ options[:scale] = 0
1419
+ :decimal
1420
+ end
1421
+ when String
1422
+ :text
1423
+ when Time
1424
+ :time
1425
+ else
1426
+ raise ArgumentError, "column type not supported yet: #{dtype}"
1427
+ end
1428
+ t.column c, column_type, **options
1429
+ end
1430
+ end
1431
+ end
1432
+
1433
+ quoted_table = connection.quote_table_name(table_name)
1434
+ quoted_columns = columns.map { |c| connection.quote_column_name(c) }
1435
+ rows = cast({Polars::UInt64 => Polars::String}).rows(named: false).map { |row| "(#{row.map { |v| connection.quote(v) }.join(", ")})" }
1436
+ connection.exec_update("INSERT INTO #{quoted_table} (#{quoted_columns.join(", ")}) VALUES #{rows.join(", ")}")
1437
+ end
1438
+ end
1439
+ end
1440
+
1441
+ # Write DataFrame to an Iceberg table.
1442
+ #
1443
+ # @note
1444
+ # This functionality is currently considered **unstable**. It may be
1445
+ # changed at any point without it being considered a breaking change.
1446
+ #
1447
+ # @param target [Object]
1448
+ # Name of the table or the Table object representing an Iceberg table.
1449
+ # @param mode ['append', 'overwrite']
1450
+ # How to handle existing data.
1451
+ #
1452
+ # - If 'append', will add new data.
1453
+ # - If 'overwrite', will replace table with new data.
1454
+ #
1455
+ # @return [nil]
1456
+ def write_iceberg(target, mode:)
1457
+ require "iceberg"
1458
+
1459
+ table =
1460
+ if target.is_a?(Iceberg::Table)
1461
+ target
1462
+ else
1463
+ raise Todo
1464
+ end
1465
+
1466
+ data = self
1467
+
1468
+ if mode == "append"
1469
+ table.append(data)
1470
+ else
1471
+ raise Todo
1472
+ end
1473
+ end
1474
+
1475
+ # Write DataFrame as delta table.
1476
+ #
1477
+ # @param target [Object]
1478
+ # URI of a table or a DeltaTable object.
1479
+ # @param mode ["error", "append", "overwrite", "ignore", "merge"]
1480
+ # How to handle existing data.
1481
+ # @param storage_options [Hash]
1482
+ # Extra options for the storage backends supported by `deltalake-rb`.
1483
+ # @param delta_write_options [Hash]
1484
+ # Additional keyword arguments while writing a Delta lake Table.
1485
+ # @param delta_merge_options [Hash]
1486
+ # Keyword arguments which are required to `MERGE` a Delta lake Table.
1487
+ #
1488
+ # @return [nil]
1489
+ def write_delta(
1490
+ target,
1491
+ mode: "error",
1492
+ storage_options: nil,
1493
+ delta_write_options: nil,
1494
+ delta_merge_options: nil
1495
+ )
1496
+ Polars.send(:_check_if_delta_available)
1497
+
1498
+ if Utils.pathlike?(target)
1499
+ target = Polars.send(:_resolve_delta_lake_uri, target.to_s, strict: false)
1500
+ end
1501
+
1502
+ data = self
1503
+
1504
+ if mode == "merge"
1505
+ if delta_merge_options.nil?
1506
+ msg = "You need to pass delta_merge_options with at least a given predicate for `MERGE` to work."
1507
+ raise ArgumentError, msg
1508
+ end
1509
+ if target.is_a?(::String)
1510
+ dt = DeltaLake::Table.new(target, storage_options: storage_options)
1511
+ else
1512
+ dt = target
1513
+ end
1514
+
1515
+ predicate = delta_merge_options.delete(:predicate)
1516
+ dt.merge(data, predicate, **delta_merge_options)
1517
+ else
1518
+ delta_write_options ||= {}
1519
+
1520
+ DeltaLake.write(
1521
+ target,
1522
+ data,
1523
+ mode: mode,
1524
+ storage_options: storage_options,
1525
+ **delta_write_options
1526
+ )
1527
+ end
1528
+ end
1529
+
964
1530
  # Return an estimation of the total (heap) allocated size of the DataFrame.
965
1531
  #
966
1532
  # Estimated size is given in the specified unit (bytes by default).
@@ -988,7 +1554,7 @@ module Polars
988
1554
  # "y" => 1_000_000.times.map { |v| v / 1000.0 },
989
1555
  # "z" => 1_000_000.times.map(&:to_s)
990
1556
  # },
991
- # columns: {"x" => :u32, "y" => :f64, "z" => :str}
1557
+ # schema: {"x" => Polars::UInt32, "y" => Polars::Float64, "z" => Polars::String}
992
1558
  # )
993
1559
  # df.estimated_size
994
1560
  # # => 25888898
@@ -1120,14 +1686,14 @@ module Polars
1120
1686
  # # │ 3 ┆ 8 ┆ c │
1121
1687
  # # └───────┴─────┴─────┘
1122
1688
  def rename(mapping, strict: true)
1123
- lazy.rename(mapping, strict: strict).collect(no_optimization: true)
1689
+ lazy.rename(mapping, strict: strict).collect(optimizations: QueryOptFlags._eager)
1124
1690
  end
1125
1691
 
1126
1692
  # Insert a Series at a certain column index. This operation is in place.
1127
1693
  #
1128
1694
  # @param index [Integer]
1129
1695
  # Column to insert the new `Series` column.
1130
- # @param series [Series]
1696
+ # @param column [Series]
1131
1697
  # `Series` to insert.
1132
1698
  #
1133
1699
  # @return [DataFrame]
@@ -1170,19 +1736,22 @@ module Polars
1170
1736
  # # │ 3 ┆ 10.0 ┆ false ┆ 20.5 │
1171
1737
  # # │ 4 ┆ 13.0 ┆ true ┆ 0.0 │
1172
1738
  # # └─────┴──────┴───────┴──────┘
1173
- def insert_column(index, series)
1739
+ def insert_column(index, column)
1174
1740
  if index < 0
1175
- index = columns.length + index
1741
+ index = width + index
1176
1742
  end
1177
- _df.insert_column(index, series._s)
1743
+ _df.insert_column(index, column._s)
1178
1744
  self
1179
1745
  end
1180
- alias_method :insert_at_idx, :insert_column
1181
1746
 
1182
1747
  # Filter the rows in the DataFrame based on a predicate expression.
1183
1748
  #
1184
- # @param predicate [Expr]
1185
- # Expression that evaluates to a boolean Series.
1749
+ # @param predicates [Array]
1750
+ # Expression(s) that evaluate to a boolean Series.
1751
+ # @param constraints [Hash]
1752
+ # Column filters; use `name = value` to filter columns by the supplied value.
1753
+ # Each constraint will behave the same as `Polars.col(name).eq(value)`, and
1754
+ # be implicitly joined with the other filter conditions using `&`.
1186
1755
  #
1187
1756
  # @return [DataFrame]
1188
1757
  #
@@ -1217,86 +1786,307 @@ module Polars
1217
1786
  # # ╞═════╪═════╪═════╡
1218
1787
  # # │ 1 ┆ 6 ┆ a │
1219
1788
  # # └─────┴─────┴─────┘
1220
- def filter(predicate)
1221
- lazy.filter(predicate).collect
1789
+ def filter(*predicates, **constraints)
1790
+ lazy.filter(*predicates, **constraints).collect(optimizations: QueryOptFlags._eager)
1222
1791
  end
1223
1792
 
1224
- # Summary statistics for a DataFrame.
1793
+ # Remove rows, dropping those that match the given predicate expression(s).
1794
+ #
1795
+ # The original order of the remaining rows is preserved.
1796
+ #
1797
+ # Rows where the filter predicate does not evaluate to true are retained
1798
+ # (this includes rows where the predicate evaluates as `null`).
1799
+ #
1800
+ # @param predicates [Array]
1801
+ # Expression that evaluates to a boolean Series.
1802
+ # @param constraints [Hash]
1803
+ # Column filters; use `name = value` to filter columns using the supplied
1804
+ # value. Each constraint behaves the same as `Polars.col(name).eq(value)`,
1805
+ # and is implicitly joined with the other filter conditions using `&`.
1225
1806
  #
1226
1807
  # @return [DataFrame]
1227
1808
  #
1809
+ # @example Remove rows matching a condition:
1810
+ # df = Polars::DataFrame.new(
1811
+ # {
1812
+ # "foo" => [2, 3, nil, 4, 0],
1813
+ # "bar" => [5, 6, nil, nil, 0],
1814
+ # "ham" => ["a", "b", nil, "c", "d"]
1815
+ # }
1816
+ # )
1817
+ # df.remove(Polars.col("bar") >= 5)
1818
+ # # =>
1819
+ # # shape: (3, 3)
1820
+ # # ┌──────┬──────┬──────┐
1821
+ # # │ foo ┆ bar ┆ ham │
1822
+ # # │ --- ┆ --- ┆ --- │
1823
+ # # │ i64 ┆ i64 ┆ str │
1824
+ # # ╞══════╪══════╪══════╡
1825
+ # # │ null ┆ null ┆ null │
1826
+ # # │ 4 ┆ null ┆ c │
1827
+ # # │ 0 ┆ 0 ┆ d │
1828
+ # # └──────┴──────┴──────┘
1829
+ #
1830
+ # @example Discard rows based on multiple conditions, combined with and/or operators:
1831
+ # df.remove(
1832
+ # (Polars.col("foo") >= 0) & (Polars.col("bar") >= 0),
1833
+ # )
1834
+ # # =>
1835
+ # # shape: (2, 3)
1836
+ # # ┌──────┬──────┬──────┐
1837
+ # # │ foo ┆ bar ┆ ham │
1838
+ # # │ --- ┆ --- ┆ --- │
1839
+ # # │ i64 ┆ i64 ┆ str │
1840
+ # # ╞══════╪══════╪══════╡
1841
+ # # │ null ┆ null ┆ null │
1842
+ # # │ 4 ┆ null ┆ c │
1843
+ # # └──────┴──────┴──────┘
1844
+ #
1228
1845
  # @example
1846
+ # df.remove(
1847
+ # (Polars.col("foo") >= 0) | (Polars.col("bar") >= 0),
1848
+ # )
1849
+ # # =>
1850
+ # # shape: (1, 3)
1851
+ # # ┌──────┬──────┬──────┐
1852
+ # # │ foo ┆ bar ┆ ham │
1853
+ # # │ --- ┆ --- ┆ --- │
1854
+ # # │ i64 ┆ i64 ┆ str │
1855
+ # # ╞══════╪══════╪══════╡
1856
+ # # │ null ┆ null ┆ null │
1857
+ # # └──────┴──────┴──────┘
1858
+ #
1859
+ # @example Provide multiple constraints using `*args` syntax:
1860
+ # df.remove(
1861
+ # Polars.col("ham").is_not_null,
1862
+ # Polars.col("bar") >= 0
1863
+ # )
1864
+ # # =>
1865
+ # # shape: (2, 3)
1866
+ # # ┌──────┬──────┬──────┐
1867
+ # # │ foo ┆ bar ┆ ham │
1868
+ # # │ --- ┆ --- ┆ --- │
1869
+ # # │ i64 ┆ i64 ┆ str │
1870
+ # # ╞══════╪══════╪══════╡
1871
+ # # │ null ┆ null ┆ null │
1872
+ # # │ 4 ┆ null ┆ c │
1873
+ # # └──────┴──────┴──────┘
1874
+ #
1875
+ # @example Provide constraints(s) using `**kwargs` syntax:
1876
+ # df.remove(foo: 0, bar: 0)
1877
+ # # =>
1878
+ # # shape: (4, 3)
1879
+ # # ┌──────┬──────┬──────┐
1880
+ # # │ foo ┆ bar ┆ ham │
1881
+ # # │ --- ┆ --- ┆ --- │
1882
+ # # │ i64 ┆ i64 ┆ str │
1883
+ # # ╞══════╪══════╪══════╡
1884
+ # # │ 2 ┆ 5 ┆ a │
1885
+ # # │ 3 ┆ 6 ┆ b │
1886
+ # # │ null ┆ null ┆ null │
1887
+ # # │ 4 ┆ null ┆ c │
1888
+ # # └──────┴──────┴──────┘
1889
+ #
1890
+ # @example Remove rows by comparing two columns against each other:
1891
+ # df.remove(
1892
+ # Polars.col("foo").ne_missing(Polars.col("bar"))
1893
+ # )
1894
+ # # =>
1895
+ # # shape: (2, 3)
1896
+ # # ┌──────┬──────┬──────┐
1897
+ # # │ foo ┆ bar ┆ ham │
1898
+ # # │ --- ┆ --- ┆ --- │
1899
+ # # │ i64 ┆ i64 ┆ str │
1900
+ # # ╞══════╪══════╪══════╡
1901
+ # # │ null ┆ null ┆ null │
1902
+ # # │ 0 ┆ 0 ┆ d │
1903
+ # # └──────┴──────┴──────┘
1904
+ def remove(
1905
+ *predicates,
1906
+ **constraints
1907
+ )
1908
+ lazy
1909
+ .remove(*predicates, **constraints)
1910
+ .collect(optimizations: QueryOptFlags._eager)
1911
+ end
1912
+
1913
+ # Return a dense preview of the DataFrame.
1914
+ #
1915
+ # The formatting shows one line per column so that wide dataframes display
1916
+ # cleanly. Each line shows the column name, the data type, and the first
1917
+ # few values.
1918
+ #
1919
+ # @param max_items_per_column [Integer]
1920
+ # Maximum number of items to show per column.
1921
+ # @param max_colname_length [Integer]
1922
+ # Maximum length of the displayed column names; values that exceed
1923
+ # this value are truncated with a trailing ellipsis.
1924
+ # @param return_type [nil, 'self', 'frame', 'string']
1925
+ # Modify the return format:
1926
+ #
1927
+ # - `nil` (default): Print the glimpse output to stdout, returning `nil`.
1928
+ # - `"self"`: Print the glimpse output to stdout, returning the *original* frame.
1929
+ # - `"frame"`: Return the glimpse output as a new DataFrame.
1930
+ # - `"string"`: Return the glimpse output as a string.
1931
+ #
1932
+ # @return [Object]
1933
+ #
1934
+ # @example Return the glimpse output as a DataFrame:
1229
1935
  # df = Polars::DataFrame.new(
1230
1936
  # {
1231
1937
  # "a" => [1.0, 2.8, 3.0],
1232
1938
  # "b" => [4, 5, nil],
1233
1939
  # "c" => [true, false, true],
1234
1940
  # "d" => [nil, "b", "c"],
1235
- # "e" => ["usd", "eur", nil]
1941
+ # "e" => ["usd", "eur", nil],
1942
+ # "f" => [Date.new(2020, 1, 1), Date.new(2021, 1, 2), Date.new(2022, 1, 1)]
1236
1943
  # }
1237
1944
  # )
1238
- # df.describe
1945
+ # df.glimpse(return_type: "frame")
1239
1946
  # # =>
1240
- # # shape: (7, 6)
1241
- # # ┌────────────┬──────────┬──────────┬──────────┬──────┬──────┐
1242
- # # │ describe ┆ a b ┆ c d ┆ e
1243
- # # │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ ---
1244
- # # │ str f64 ┆ f64 ┆ f64 ┆ str ┆ str
1245
- # # ╞════════════╪══════════╪══════════╪══════════╪══════╪══════╡
1246
- # # │ count3.0 3.0 3.0 ┆ 3 ┆ 3
1247
- # # │ null_count ┆ 0.01.0 0.0 ┆ 1 ┆ 1
1248
- # # │ mean ┆ 2.266667 ┆ 4.50.666667 null null
1249
- # # │ std 1.101514 0.707107 ┆ 0.57735 ┆ null null
1250
- # # │ min ┆ 1.04.0 0.0 ┆ b ┆ eur
1251
- # # │ max ┆ 3.05.0 1.0 ┆ c ┆ usd
1252
- # # │ median ┆ 2.8 ┆ 4.5 ┆ 1.0 ┆ null ┆ null │
1253
- # # └────────────┴──────────┴──────────┴──────────┴──────┴──────┘
1254
- def describe
1255
- describe_cast = lambda do |stat|
1256
- columns = []
1257
- self.columns.each_with_index do |s, i|
1258
- if self[s].is_numeric || self[s].is_boolean
1259
- columns << stat[0.., i].cast(:f64)
1260
- else
1261
- # for dates, strings, etc, we cast to string so that all
1262
- # statistics can be shown
1263
- columns << stat[0.., i].cast(:str)
1264
- end
1947
+ # # shape: (6, 3)
1948
+ # # ┌────────┬───────┬─────────────────────────────────┐
1949
+ # # │ columndtypevalues
1950
+ # # │ --- ┆ --- ┆ ---
1951
+ # # │ str ┆ str list[str]
1952
+ # # ╞════════╪═══════╪═════════════════════════════════╡
1953
+ # # │ af64 ["1.0", "2.8", "3.0"]
1954
+ # # │ bi64 ["4", "5", null]
1955
+ # # │ cbool ["true", "false", "true"]
1956
+ # # │ d str [null, ""b"", ""c""]
1957
+ # # │ estr [""usd"", ""eur"", null]
1958
+ # # │ fdate ["2020-01-01", "2021-01-02", "…
1959
+ # # └────────┴───────┴─────────────────────────────────┘
1960
+ def glimpse(
1961
+ max_items_per_column: 10,
1962
+ max_colname_length: 50,
1963
+ return_type: nil
1964
+ )
1965
+ if return_type.nil?
1966
+ return_frame = false
1967
+ else
1968
+ return_frame = return_type == "frame"
1969
+ if !return_frame && !["self", "string"].include?(return_type)
1970
+ msg = "invalid `return_type`; found #{return_type.inspect}, expected one of 'string', 'frame', 'self', or nil"
1971
+ raise ArgumentError, msg
1265
1972
  end
1266
- self.class.new(columns)
1267
1973
  end
1268
1974
 
1269
- summary = _from_rbdf(
1270
- Polars.concat(
1271
- [
1272
- describe_cast.(
1273
- self.class.new(columns.to_h { |c| [c, [height]] })
1274
- ),
1275
- describe_cast.(null_count),
1276
- describe_cast.(mean),
1277
- describe_cast.(std),
1278
- describe_cast.(min),
1279
- describe_cast.(max),
1280
- describe_cast.(median)
1281
- ]
1282
- )._df
1283
- )
1284
- summary.insert_column(
1285
- 0,
1286
- Polars::Series.new(
1287
- "describe",
1288
- ["count", "null_count", "mean", "std", "min", "max", "median"],
1975
+ # always print at most this number of values (mainly ensures that
1976
+ # we do not cast long arrays to strings, which would be slow)
1977
+ max_n_values = [max_items_per_column, height].min
1978
+ schema = self.schema
1979
+
1980
+ _column_to_row_output = lambda do |col_name, dtype|
1981
+ fn = schema[col_name] == String ? :inspect : :to_s
1982
+ values = self[0...max_n_values, col_name].to_a
1983
+ if col_name.length > max_colname_length
1984
+ col_name = col_name[0...(max_colname_length - 1)] + "…"
1985
+ end
1986
+ dtype_str = Plr.dtype_str_repr(dtype)
1987
+ if !return_frame
1988
+ dtype_str = "<#{dtype_str}>"
1989
+ end
1990
+ [col_name, dtype_str, values.map { |v| !v.nil? ? v.send(fn) : nil }]
1991
+ end
1992
+
1993
+ data = self.schema.map { |s, dtype| _column_to_row_output.(s, dtype) }
1994
+
1995
+ # output one row per column
1996
+ if return_frame
1997
+ DataFrame.new(
1998
+ data,
1999
+ orient: "row",
2000
+ schema: {"column" => String, "dtype" => String, "values" => List.new(String)}
1289
2001
  )
1290
- )
1291
- summary
2002
+ else
2003
+ raise Todo
2004
+ end
1292
2005
  end
1293
2006
 
1294
- # Find the index of a column by name.
2007
+ # Summary statistics for a DataFrame.
1295
2008
  #
1296
- # @param name [String]
1297
- # Name of the column to find.
2009
+ # @param percentiles [Array]
2010
+ # One or more percentiles to include in the summary statistics.
2011
+ # All values must be in the range `[0, 1]`.
2012
+ # @param interpolation ['nearest', 'higher', 'lower', 'midpoint', 'linear', 'equiprobable']
2013
+ # Interpolation method used when calculating percentiles.
1298
2014
  #
1299
- # @return [Series]
2015
+ # @return [DataFrame]
2016
+ #
2017
+ # @example Show default frame statistics:
2018
+ # df = Polars::DataFrame.new(
2019
+ # {
2020
+ # "float" => [1.0, 2.8, 3.0],
2021
+ # "int" => [40, 50, nil],
2022
+ # "bool" => [true, false, true],
2023
+ # "str" => ["zz", "xx", "yy"],
2024
+ # "date" => [Date.new(2020, 1, 1), Date.new(2021, 7, 5), Date.new(2022, 12, 31)]
2025
+ # }
2026
+ # )
2027
+ # df.describe
2028
+ # # =>
2029
+ # # shape: (9, 6)
2030
+ # # ┌────────────┬──────────┬──────────┬──────────┬──────┬─────────────────────────┐
2031
+ # # │ statistic ┆ float ┆ int ┆ bool ┆ str ┆ date │
2032
+ # # │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
2033
+ # # │ str ┆ f64 ┆ f64 ┆ f64 ┆ str ┆ str │
2034
+ # # ╞════════════╪══════════╪══════════╪══════════╪══════╪═════════════════════════╡
2035
+ # # │ count ┆ 3.0 ┆ 2.0 ┆ 3.0 ┆ 3 ┆ 3 │
2036
+ # # │ null_count ┆ 0.0 ┆ 1.0 ┆ 0.0 ┆ 0 ┆ 0 │
2037
+ # # │ mean ┆ 2.266667 ┆ 45.0 ┆ 0.666667 ┆ null ┆ 2021-07-02 16:00:00 UTC │
2038
+ # # │ std ┆ 1.101514 ┆ 7.071068 ┆ null ┆ null ┆ null │
2039
+ # # │ min ┆ 1.0 ┆ 40.0 ┆ 0.0 ┆ xx ┆ 2020-01-01 │
2040
+ # # │ 25% ┆ 2.8 ┆ 40.0 ┆ null ┆ null ┆ 2021-07-05 │
2041
+ # # │ 50% ┆ 2.8 ┆ 50.0 ┆ null ┆ null ┆ 2021-07-05 │
2042
+ # # │ 75% ┆ 3.0 ┆ 50.0 ┆ null ┆ null ┆ 2022-12-31 │
2043
+ # # │ max ┆ 3.0 ┆ 50.0 ┆ 1.0 ┆ zz ┆ 2022-12-31 │
2044
+ # # └────────────┴──────────┴──────────┴──────────┴──────┴─────────────────────────┘
2045
+ #
2046
+ # @example Customize which percentiles are displayed, applying linear interpolation:
2047
+ # df.describe(
2048
+ # percentiles: [0.1, 0.3, 0.5, 0.7, 0.9],
2049
+ # interpolation: "linear"
2050
+ # )
2051
+ # # =>
2052
+ # # shape: (11, 6)
2053
+ # # ┌────────────┬──────────┬──────────┬──────────┬──────┬─────────────────────────┐
2054
+ # # │ statistic ┆ float ┆ int ┆ bool ┆ str ┆ date │
2055
+ # # │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
2056
+ # # │ str ┆ f64 ┆ f64 ┆ f64 ┆ str ┆ str │
2057
+ # # ╞════════════╪══════════╪══════════╪══════════╪══════╪═════════════════════════╡
2058
+ # # │ count ┆ 3.0 ┆ 2.0 ┆ 3.0 ┆ 3 ┆ 3 │
2059
+ # # │ null_count ┆ 0.0 ┆ 1.0 ┆ 0.0 ┆ 0 ┆ 0 │
2060
+ # # │ mean ┆ 2.266667 ┆ 45.0 ┆ 0.666667 ┆ null ┆ 2021-07-02 16:00:00 UTC │
2061
+ # # │ std ┆ 1.101514 ┆ 7.071068 ┆ null ┆ null ┆ null │
2062
+ # # │ min ┆ 1.0 ┆ 40.0 ┆ 0.0 ┆ xx ┆ 2020-01-01 │
2063
+ # # │ … ┆ … ┆ … ┆ … ┆ … ┆ … │
2064
+ # # │ 30% ┆ 2.08 ┆ 43.0 ┆ null ┆ null ┆ 2020-11-26 │
2065
+ # # │ 50% ┆ 2.8 ┆ 45.0 ┆ null ┆ null ┆ 2021-07-05 │
2066
+ # # │ 70% ┆ 2.88 ┆ 47.0 ┆ null ┆ null ┆ 2022-02-07 │
2067
+ # # │ 90% ┆ 2.96 ┆ 49.0 ┆ null ┆ null ┆ 2022-09-13 │
2068
+ # # │ max ┆ 3.0 ┆ 50.0 ┆ 1.0 ┆ zz ┆ 2022-12-31 │
2069
+ # # └────────────┴──────────┴──────────┴──────────┴──────┴─────────────────────────┘
2070
+ def describe(
2071
+ percentiles: [0.25, 0.5, 0.75],
2072
+ interpolation: "nearest"
2073
+ )
2074
+ if columns.empty?
2075
+ msg = "cannot describe a DataFrame that has no columns"
2076
+ raise TypeError, msg
2077
+ end
2078
+
2079
+ lazy.describe(
2080
+ percentiles: percentiles, interpolation: interpolation
2081
+ )
2082
+ end
2083
+
2084
+ # Find the index of a column by name.
2085
+ #
2086
+ # @param name [String]
2087
+ # Name of the column to find.
2088
+ #
2089
+ # @return [Series]
1300
2090
  #
1301
2091
  # @example
1302
2092
  # df = Polars::DataFrame.new(
@@ -1307,13 +2097,12 @@ module Polars
1307
2097
  def get_column_index(name)
1308
2098
  _df.get_column_index(name)
1309
2099
  end
1310
- alias_method :find_idx_by_name, :get_column_index
1311
2100
 
1312
2101
  # Replace a column at an index location.
1313
2102
  #
1314
2103
  # @param index [Integer]
1315
2104
  # Column index.
1316
- # @param series [Series]
2105
+ # @param column [Series]
1317
2106
  # Series that will replace the column.
1318
2107
  #
1319
2108
  # @return [DataFrame]
@@ -1339,23 +2128,31 @@ module Polars
1339
2128
  # # │ 20 ┆ 7 ┆ b │
1340
2129
  # # │ 30 ┆ 8 ┆ c │
1341
2130
  # # └───────┴─────┴─────┘
1342
- def replace_column(index, series)
2131
+ def replace_column(index, column)
1343
2132
  if index < 0
1344
- index = columns.length + index
2133
+ index = width + index
1345
2134
  end
1346
- _df.replace_column(index, series._s)
2135
+ _df.replace_column(index, column._s)
1347
2136
  self
1348
2137
  end
1349
- alias_method :replace_at_idx, :replace_column
1350
2138
 
1351
- # Sort the DataFrame by column.
2139
+ # Sort the dataframe by the given columns.
1352
2140
  #
1353
- # @param by [String]
1354
- # By which column to sort.
1355
- # @param reverse [Boolean]
1356
- # Reverse/descending sort.
2141
+ # @param by [Object]
2142
+ # Column(s) to sort by. Accepts expression input, including selectors. Strings
2143
+ # are parsed as column names.
2144
+ # @param more_by [Array]
2145
+ # Additional columns to sort by, specified as positional arguments.
2146
+ # @param descending [Boolean]
2147
+ # Sort in descending order. When sorting by multiple columns, can be specified
2148
+ # per column by passing an array of booleans.
1357
2149
  # @param nulls_last [Boolean]
1358
- # Place null values last. Can only be used if sorted by a single column.
2150
+ # Place null values last; can specify a single boolean applying to all columns
2151
+ # or an array of booleans for per-column control.
2152
+ # @param multithreaded [Boolean]
2153
+ # Sort using multiple threads.
2154
+ # @param maintain_order [Boolean]
2155
+ # Whether the order should be maintained if elements are equal.
1359
2156
  #
1360
2157
  # @return [DataFrame]
1361
2158
  #
@@ -1367,7 +2164,7 @@ module Polars
1367
2164
  # "ham" => ["a", "b", "c"]
1368
2165
  # }
1369
2166
  # )
1370
- # df.sort("foo", reverse: true)
2167
+ # df.sort("foo", descending: true)
1371
2168
  # # =>
1372
2169
  # # shape: (3, 3)
1373
2170
  # # ┌─────┬─────┬─────┐
@@ -1383,7 +2180,7 @@ module Polars
1383
2180
  # @example Sort by multiple columns.
1384
2181
  # df.sort(
1385
2182
  # [Polars.col("foo"), Polars.col("bar")**2],
1386
- # reverse: [true, false]
2183
+ # descending: [true, false]
1387
2184
  # )
1388
2185
  # # =>
1389
2186
  # # shape: (3, 3)
@@ -1396,24 +2193,255 @@ module Polars
1396
2193
  # # │ 2 ┆ 7.0 ┆ b │
1397
2194
  # # │ 1 ┆ 6.0 ┆ a │
1398
2195
  # # └─────┴─────┴─────┘
1399
- def sort(by, reverse: false, nulls_last: false)
2196
+ def sort(
2197
+ by,
2198
+ *more_by,
2199
+ descending: false,
2200
+ nulls_last: false,
2201
+ multithreaded: true,
2202
+ maintain_order: false
2203
+ )
1400
2204
  lazy
1401
- .sort(by, reverse: reverse, nulls_last: nulls_last)
1402
- .collect(no_optimization: true)
2205
+ .sort(
2206
+ by,
2207
+ *more_by,
2208
+ descending: descending,
2209
+ nulls_last: nulls_last,
2210
+ multithreaded: multithreaded,
2211
+ maintain_order: maintain_order
2212
+ )
2213
+ .collect(optimizations: QueryOptFlags._eager)
1403
2214
  end
1404
2215
 
1405
2216
  # Sort the DataFrame by column in-place.
1406
2217
  #
1407
2218
  # @param by [String]
1408
2219
  # By which column to sort.
1409
- # @param reverse [Boolean]
2220
+ # @param descending [Boolean]
1410
2221
  # Reverse/descending sort.
1411
2222
  # @param nulls_last [Boolean]
1412
2223
  # Place null values last. Can only be used if sorted by a single column.
1413
2224
  #
1414
2225
  # @return [DataFrame]
1415
- def sort!(by, reverse: false, nulls_last: false)
1416
- self._df = sort(by, reverse: reverse, nulls_last: nulls_last)._df
2226
+ def sort!(by, descending: false, nulls_last: false)
2227
+ self._df = sort(by, descending: descending, nulls_last: nulls_last)._df
2228
+ end
2229
+
2230
+ # Execute a SQL query against the DataFrame.
2231
+ #
2232
+ # @note
2233
+ # This functionality is considered **unstable**, although it is close to
2234
+ # being considered stable. It may be changed at any point without it being
2235
+ # considered a breaking change.
2236
+ #
2237
+ # @param query [String]
2238
+ # SQL query to execute.
2239
+ # @param table_name [String]
2240
+ # Optionally provide an explicit name for the table that represents the
2241
+ # calling frame (defaults to "self").
2242
+ #
2243
+ # @return [DataFrame]
2244
+ #
2245
+ # @note
2246
+ # * The calling frame is automatically registered as a table in the SQL context
2247
+ # under the name "self". If you want access to the DataFrames and LazyFrames
2248
+ # found in the current globals, use the top-level :meth:`pl.sql <polars.sql>`.
2249
+ # * More control over registration and execution behaviour is available by
2250
+ # using the :class:`SQLContext` object.
2251
+ # * The SQL query executes in lazy mode before being collected and returned
2252
+ # as a DataFrame.
2253
+ #
2254
+ # @example Query the DataFrame using SQL:
2255
+ # df1 = Polars::DataFrame.new(
2256
+ # {
2257
+ # "a" => [1, 2, 3],
2258
+ # "b" => ["zz", "yy", "xx"],
2259
+ # "c" => [Date.new(1999, 12, 31), Date.new(2010, 10, 10), Date.new(2077, 8, 8)]
2260
+ # }
2261
+ # )
2262
+ # df1.sql("SELECT c, b FROM self WHERE a > 1")
2263
+ # # =>
2264
+ # # shape: (2, 2)
2265
+ # # ┌────────────┬─────┐
2266
+ # # │ c ┆ b │
2267
+ # # │ --- ┆ --- │
2268
+ # # │ date ┆ str │
2269
+ # # ╞════════════╪═════╡
2270
+ # # │ 2010-10-10 ┆ yy │
2271
+ # # │ 2077-08-08 ┆ xx │
2272
+ # # └────────────┴─────┘
2273
+ #
2274
+ # @example Apply transformations to a DataFrame using SQL, aliasing "self" to "frame".
2275
+ # df1.sql(
2276
+ # "
2277
+ # SELECT
2278
+ # a,
2279
+ # (a % 2 == 0) AS a_is_even,
2280
+ # CONCAT_WS(':', b, b) AS b_b,
2281
+ # EXTRACT(year FROM c) AS year,
2282
+ # 0::float4 AS \"zero\",
2283
+ # FROM frame
2284
+ # ",
2285
+ # table_name: "frame"
2286
+ # )
2287
+ # # =>
2288
+ # # shape: (3, 5)
2289
+ # # ┌─────┬───────────┬───────┬──────┬──────┐
2290
+ # # │ a ┆ a_is_even ┆ b_b ┆ year ┆ zero │
2291
+ # # │ --- ┆ --- ┆ --- ┆ --- ┆ --- │
2292
+ # # │ i64 ┆ bool ┆ str ┆ i32 ┆ f32 │
2293
+ # # ╞═════╪═══════════╪═══════╪══════╪══════╡
2294
+ # # │ 1 ┆ false ┆ zz:zz ┆ 1999 ┆ 0.0 │
2295
+ # # │ 2 ┆ true ┆ yy:yy ┆ 2010 ┆ 0.0 │
2296
+ # # │ 3 ┆ false ┆ xx:xx ┆ 2077 ┆ 0.0 │
2297
+ # # └─────┴───────────┴───────┴──────┴──────┘
2298
+ def sql(query, table_name: "self")
2299
+ ctx = SQLContext.new(eager: true)
2300
+ name = table_name || "self"
2301
+ ctx.register(name, self)
2302
+ ctx.execute(query)
2303
+ end
2304
+
2305
+ # Return the `k` largest rows.
2306
+ #
2307
+ # Non-null elements are always preferred over null elements, regardless of
2308
+ # the value of `reverse`. The output is not guaranteed to be in any
2309
+ # particular order, call `sort` after this function if you wish the
2310
+ # output to be sorted.
2311
+ #
2312
+ # @param k [Integer]
2313
+ # Number of rows to return.
2314
+ # @param by [Object]
2315
+ # Column(s) used to determine the top rows.
2316
+ # Accepts expression input. Strings are parsed as column names.
2317
+ # @param reverse [Object]
2318
+ # Consider the `k` smallest elements of the `by` column(s) (instead of the `k`
2319
+ # largest). This can be specified per column by passing an array of
2320
+ # booleans.
2321
+ #
2322
+ # @return [DataFrame]
2323
+ #
2324
+ # @example Get the rows which contain the 4 largest values in column b.
2325
+ # df = Polars::DataFrame.new(
2326
+ # {
2327
+ # "a" => ["a", "b", "a", "b", "b", "c"],
2328
+ # "b" => [2, 1, 1, 3, 2, 1]
2329
+ # }
2330
+ # )
2331
+ # df.top_k(4, by: "b")
2332
+ # # =>
2333
+ # # shape: (4, 2)
2334
+ # # ┌─────┬─────┐
2335
+ # # │ a ┆ b │
2336
+ # # │ --- ┆ --- │
2337
+ # # │ str ┆ i64 │
2338
+ # # ╞═════╪═════╡
2339
+ # # │ b ┆ 3 │
2340
+ # # │ a ┆ 2 │
2341
+ # # │ b ┆ 2 │
2342
+ # # │ b ┆ 1 │
2343
+ # # └─────┴─────┘
2344
+ #
2345
+ # @example Get the rows which contain the 4 largest values when sorting on column b and a.
2346
+ # df.top_k(4, by: ["b", "a"])
2347
+ # # =>
2348
+ # # shape: (4, 2)
2349
+ # # ┌─────┬─────┐
2350
+ # # │ a ┆ b │
2351
+ # # │ --- ┆ --- │
2352
+ # # │ str ┆ i64 │
2353
+ # # ╞═════╪═════╡
2354
+ # # │ b ┆ 3 │
2355
+ # # │ b ┆ 2 │
2356
+ # # │ a ┆ 2 │
2357
+ # # │ c ┆ 1 │
2358
+ # # └─────┴─────┘
2359
+ def top_k(
2360
+ k,
2361
+ by:,
2362
+ reverse: false
2363
+ )
2364
+ lazy
2365
+ .top_k(k, by: by, reverse: reverse)
2366
+ .collect(
2367
+ optimizations: QueryOptFlags.new(
2368
+ projection_pushdown: false,
2369
+ predicate_pushdown: false,
2370
+ comm_subplan_elim: false,
2371
+ slice_pushdown: true
2372
+ )
2373
+ )
2374
+ end
2375
+
2376
+ # Return the `k` smallest rows.
2377
+ #
2378
+ # Non-null elements are always preferred over null elements, regardless of
2379
+ # the value of `reverse`. The output is not guaranteed to be in any
2380
+ # particular order, call `sort` after this function if you wish the
2381
+ # output to be sorted.
2382
+ #
2383
+ # @param k [Integer]
2384
+ # Number of rows to return.
2385
+ # @param by [Object]
2386
+ # Column(s) used to determine the bottom rows.
2387
+ # Accepts expression input. Strings are parsed as column names.
2388
+ # @param reverse [Object]
2389
+ # Consider the `k` largest elements of the `by` column(s) (instead of the `k`
2390
+ # smallest). This can be specified per column by passing an array of
2391
+ # booleans.
2392
+ #
2393
+ # @return [DataFrame]
2394
+ #
2395
+ # @example Get the rows which contain the 4 smallest values in column b.
2396
+ # df = Polars::DataFrame.new(
2397
+ # {
2398
+ # "a" => ["a", "b", "a", "b", "b", "c"],
2399
+ # "b" => [2, 1, 1, 3, 2, 1]
2400
+ # }
2401
+ # )
2402
+ # df.bottom_k(4, by: "b")
2403
+ # # =>
2404
+ # # shape: (4, 2)
2405
+ # # ┌─────┬─────┐
2406
+ # # │ a ┆ b │
2407
+ # # │ --- ┆ --- │
2408
+ # # │ str ┆ i64 │
2409
+ # # ╞═════╪═════╡
2410
+ # # │ b ┆ 1 │
2411
+ # # │ a ┆ 1 │
2412
+ # # │ c ┆ 1 │
2413
+ # # │ a ┆ 2 │
2414
+ # # └─────┴─────┘
2415
+ #
2416
+ # @example Get the rows which contain the 4 smallest values when sorting on column a and b.
2417
+ # df.bottom_k(4, by: ["a", "b"])
2418
+ # # =>
2419
+ # # shape: (4, 2)
2420
+ # # ┌─────┬─────┐
2421
+ # # │ a ┆ b │
2422
+ # # │ --- ┆ --- │
2423
+ # # │ str ┆ i64 │
2424
+ # # ╞═════╪═════╡
2425
+ # # │ a ┆ 1 │
2426
+ # # │ a ┆ 2 │
2427
+ # # │ b ┆ 1 │
2428
+ # # │ b ┆ 2 │
2429
+ # # └─────┴─────┘
2430
+ def bottom_k(
2431
+ k,
2432
+ by:,
2433
+ reverse: false
2434
+ )
2435
+ lazy
2436
+ .bottom_k(k, by: by, reverse: reverse)
2437
+ .collect(
2438
+ optimizations: QueryOptFlags.new(
2439
+ projection_pushdown: false,
2440
+ predicate_pushdown: false,
2441
+ comm_subplan_elim: false,
2442
+ slice_pushdown: true
2443
+ )
2444
+ )
1417
2445
  end
1418
2446
 
1419
2447
  # Check if DataFrame is equal to other.
@@ -1447,36 +2475,6 @@ module Polars
1447
2475
  def equals(other, null_equal: true)
1448
2476
  _df.equals(other._df, null_equal)
1449
2477
  end
1450
- alias_method :frame_equal, :equals
1451
-
1452
- # Replace a column by a new Series.
1453
- #
1454
- # @param column [String]
1455
- # Column to replace.
1456
- # @param new_col [Series]
1457
- # New column to insert.
1458
- #
1459
- # @return [DataFrame]
1460
- #
1461
- # @example
1462
- # df = Polars::DataFrame.new({"foo" => [1, 2, 3], "bar" => [4, 5, 6]})
1463
- # s = Polars::Series.new([10, 20, 30])
1464
- # df.replace("foo", s)
1465
- # # =>
1466
- # # shape: (3, 2)
1467
- # # ┌─────┬─────┐
1468
- # # │ foo ┆ bar │
1469
- # # │ --- ┆ --- │
1470
- # # │ i64 ┆ i64 │
1471
- # # ╞═════╪═════╡
1472
- # # │ 10 ┆ 4 │
1473
- # # │ 20 ┆ 5 │
1474
- # # │ 30 ┆ 6 │
1475
- # # └─────┴─────┘
1476
- def replace(column, new_col)
1477
- _df.replace(column.to_s, new_col._s)
1478
- self
1479
- end
1480
2478
 
1481
2479
  # Get a slice of this DataFrame.
1482
2480
  #
@@ -1606,10 +2604,59 @@ module Polars
1606
2604
  _from_rbdf(_df.tail(n))
1607
2605
  end
1608
2606
 
1609
- # Return a new DataFrame where the null values are dropped.
2607
+ # Drop all rows that contain one or more NaN values.
2608
+ #
2609
+ # The original order of the remaining rows is preserved.
2610
+ #
2611
+ # @param subset [Object]
2612
+ # Column name(s) for which NaN values are considered; if set to `nil`
2613
+ # (default), use all columns (note that only floating-point columns
2614
+ # can contain NaNs).
2615
+ #
2616
+ # @return [DataFrame]
2617
+ #
2618
+ # @example
2619
+ # df = Polars::DataFrame.new(
2620
+ # {
2621
+ # "foo" => [-20.5, Float::NAN, 80.0],
2622
+ # "bar" => [Float::NAN, 110.0, 25.5],
2623
+ # "ham" => ["xxx", "yyy", nil]
2624
+ # }
2625
+ # )
2626
+ # df.drop_nans
2627
+ # # =>
2628
+ # # shape: (1, 3)
2629
+ # # ┌──────┬──────┬──────┐
2630
+ # # │ foo ┆ bar ┆ ham │
2631
+ # # │ --- ┆ --- ┆ --- │
2632
+ # # │ f64 ┆ f64 ┆ str │
2633
+ # # ╞══════╪══════╪══════╡
2634
+ # # │ 80.0 ┆ 25.5 ┆ null │
2635
+ # # └──────┴──────┴──────┘
2636
+ #
2637
+ # @example
2638
+ # df.drop_nans(subset: ["bar"])
2639
+ # # =>
2640
+ # # shape: (2, 3)
2641
+ # # ┌──────┬───────┬──────┐
2642
+ # # │ foo ┆ bar ┆ ham │
2643
+ # # │ --- ┆ --- ┆ --- │
2644
+ # # │ f64 ┆ f64 ┆ str │
2645
+ # # ╞══════╪═══════╪══════╡
2646
+ # # │ NaN ┆ 110.0 ┆ yyy │
2647
+ # # │ 80.0 ┆ 25.5 ┆ null │
2648
+ # # └──────┴───────┴──────┘
2649
+ def drop_nans(subset: nil)
2650
+ lazy.drop_nans(subset: subset).collect(optimizations: QueryOptFlags._eager)
2651
+ end
2652
+
2653
+ # Drop all rows that contain one or more null values.
2654
+ #
2655
+ # The original order of the remaining rows is preserved.
1610
2656
  #
1611
2657
  # @param subset [Object]
1612
- # Subset of column(s) on which `drop_nulls` will be applied.
2658
+ # Column name(s) for which null values are considered.
2659
+ # If set to `nil` (default), use all columns.
1613
2660
  #
1614
2661
  # @return [DataFrame]
1615
2662
  #
@@ -1618,27 +2665,39 @@ module Polars
1618
2665
  # {
1619
2666
  # "foo" => [1, 2, 3],
1620
2667
  # "bar" => [6, nil, 8],
1621
- # "ham" => ["a", "b", "c"]
2668
+ # "ham" => ["a", "b", nil]
1622
2669
  # }
1623
2670
  # )
1624
2671
  # df.drop_nulls
1625
2672
  # # =>
1626
- # # shape: (2, 3)
2673
+ # # shape: (1, 3)
1627
2674
  # # ┌─────┬─────┬─────┐
1628
2675
  # # │ foo ┆ bar ┆ ham │
1629
2676
  # # │ --- ┆ --- ┆ --- │
1630
2677
  # # │ i64 ┆ i64 ┆ str │
1631
2678
  # # ╞═════╪═════╪═════╡
1632
2679
  # # │ 1 ┆ 6 ┆ a │
1633
- # # │ 3 ┆ 8 ┆ c │
1634
2680
  # # └─────┴─────┴─────┘
2681
+ #
2682
+ # @example
2683
+ # df.drop_nulls(subset: Polars.cs.integer)
2684
+ # # =>
2685
+ # # shape: (2, 3)
2686
+ # # ┌─────┬─────┬──────┐
2687
+ # # │ foo ┆ bar ┆ ham │
2688
+ # # │ --- ┆ --- ┆ --- │
2689
+ # # │ i64 ┆ i64 ┆ str │
2690
+ # # ╞═════╪═════╪══════╡
2691
+ # # │ 1 ┆ 6 ┆ a │
2692
+ # # │ 3 ┆ 8 ┆ null │
2693
+ # # └─────┴─────┴──────┘
1635
2694
  def drop_nulls(subset: nil)
1636
- lazy.drop_nulls(subset: subset).collect(_eager: true)
2695
+ lazy.drop_nulls(subset: subset).collect(optimizations: QueryOptFlags._eager)
1637
2696
  end
1638
2697
 
1639
2698
  # Offers a structured way to apply a sequence of user-defined functions (UDFs).
1640
2699
  #
1641
- # @param func [Object]
2700
+ # @param function [Object]
1642
2701
  # Callable; will receive the frame as the first parameter,
1643
2702
  # followed by any given args/kwargs.
1644
2703
  # @param args [Object]
@@ -1655,7 +2714,7 @@ module Polars
1655
2714
  #
1656
2715
  # @example
1657
2716
  # cast_str_to_int = lambda do |data, col_name:|
1658
- # data.with_column(Polars.col(col_name).cast(:i64))
2717
+ # data.with_columns(Polars.col(col_name).cast(Polars::Int64))
1659
2718
  # end
1660
2719
  #
1661
2720
  # df = Polars::DataFrame.new({"a" => [1, 2, 3, 4], "b" => ["10", "20", "30", "40"]})
@@ -1672,8 +2731,94 @@ module Polars
1672
2731
  # # │ 3 ┆ 30 │
1673
2732
  # # │ 4 ┆ 40 │
1674
2733
  # # └─────┴─────┘
1675
- def pipe(func, *args, **kwargs, &block)
1676
- func.call(self, *args, **kwargs, &block)
2734
+ def pipe(function, *args, **kwargs, &block)
2735
+ function.(self, *args, **kwargs, &block)
2736
+ end
2737
+
2738
+ # Apply eager functions to columns of a DataFrame.
2739
+ #
2740
+ # Users should always prefer :meth:`with_columns` unless they are using
2741
+ # expressions that are only possible on `Series` and not on `Expr`. This is almost
2742
+ # never the case, except for a very select few functions that cannot know the
2743
+ # output datatype without looking at the data.
2744
+ #
2745
+ # @param column_names [Object]
2746
+ # The columns to apply the UDF to.
2747
+ # @param args [Array]
2748
+ # Arguments to pass to the UDF.
2749
+ # @param kwargs [Hash]
2750
+ # Keyword arguments to pass to the UDF.
2751
+ #
2752
+ # @return [DataFrame]
2753
+ #
2754
+ # @example
2755
+ # df = Polars::DataFrame.new({"a" => [1, 2, 3, 4], "b" => ["10", "20", "30", "40"]})
2756
+ # df.map_columns("a") { |s| s.shrink_dtype }
2757
+ # # =>
2758
+ # # shape: (4, 2)
2759
+ # # ┌─────┬─────┐
2760
+ # # │ a ┆ b │
2761
+ # # │ --- ┆ --- │
2762
+ # # │ i8 ┆ str │
2763
+ # # ╞═════╪═════╡
2764
+ # # │ 1 ┆ 10 │
2765
+ # # │ 2 ┆ 20 │
2766
+ # # │ 3 ┆ 30 │
2767
+ # # │ 4 ┆ 40 │
2768
+ # # └─────┴─────┘
2769
+ #
2770
+ # @example
2771
+ # df = Polars::DataFrame.new(
2772
+ # {
2773
+ # "a" => ['{"x":"a"}', nil, '{"x":"b"}', nil],
2774
+ # "b" => ['{"a":1, "b": true}', nil, '{"a":2, "b": false}', nil]
2775
+ # }
2776
+ # )
2777
+ # df.map_columns(["a", "b"]) { |s| s.str.json_decode }
2778
+ # # =>
2779
+ # # shape: (4, 2)
2780
+ # # ┌───────────┬───────────┐
2781
+ # # │ a ┆ b │
2782
+ # # │ --- ┆ --- │
2783
+ # # │ struct[1] ┆ struct[2] │
2784
+ # # ╞═══════════╪═══════════╡
2785
+ # # │ {"a"} ┆ {1,true} │
2786
+ # # │ null ┆ null │
2787
+ # # │ {"b"} ┆ {2,false} │
2788
+ # # │ null ┆ null │
2789
+ # # └───────────┴───────────┘
2790
+ #
2791
+ # @example
2792
+ # df.map_columns(Polars.cs.all) { |s| s.str.json_decode }
2793
+ # # =>
2794
+ # # shape: (4, 2)
2795
+ # # ┌───────────┬───────────┐
2796
+ # # │ a ┆ b │
2797
+ # # │ --- ┆ --- │
2798
+ # # │ struct[1] ┆ struct[2] │
2799
+ # # ╞═══════════╪═══════════╡
2800
+ # # │ {"a"} ┆ {1,true} │
2801
+ # # │ null ┆ null │
2802
+ # # │ {"b"} ┆ {2,false} │
2803
+ # # │ null ┆ null │
2804
+ # # └───────────┴───────────┘
2805
+ def map_columns(
2806
+ column_names,
2807
+ *args,
2808
+ **kwargs,
2809
+ &function
2810
+ )
2811
+ if column_names.is_a?(Selector) || column_names.is_a?(Expr)
2812
+ c_names = Array(Utils.expand_selector(self, column_names))
2813
+ elsif Utils.strlike?(column_names)
2814
+ c_names = [column_names]
2815
+ else
2816
+ c_names = Array(column_names)
2817
+ end
2818
+
2819
+ with_columns(
2820
+ c_names.map { |c| function.call(self[c], *args, **kwargs) }
2821
+ )
1677
2822
  end
1678
2823
 
1679
2824
  # Add a column at index 0 that counts the rows.
@@ -1707,28 +2852,30 @@ module Polars
1707
2852
  def with_row_index(name: "index", offset: 0)
1708
2853
  _from_rbdf(_df.with_row_index(name, offset))
1709
2854
  end
1710
- alias_method :with_row_count, :with_row_index
1711
2855
 
1712
2856
  # Start a group by operation.
1713
2857
  #
1714
- # @param by [Object]
2858
+ # @param by [Array]
1715
2859
  # Column(s) to group by.
1716
2860
  # @param maintain_order [Boolean]
1717
2861
  # Make sure that the order of the groups remain consistent. This is more
1718
2862
  # expensive than a default group by. Note that this only works in expression
1719
2863
  # aggregations.
2864
+ # @param named_by [Hash]
2865
+ # Additional columns to group by, specified as keyword arguments.
2866
+ # The columns will be renamed to the keyword used.
1720
2867
  #
1721
2868
  # @return [GroupBy]
1722
2869
  #
1723
- # @example
2870
+ # @example Group by one column and call `agg` to compute the grouped sum of another column.
1724
2871
  # df = Polars::DataFrame.new(
1725
2872
  # {
1726
- # "a" => ["a", "b", "a", "b", "b", "c"],
1727
- # "b" => [1, 2, 3, 4, 5, 6],
1728
- # "c" => [6, 5, 4, 3, 2, 1]
2873
+ # "a" => ["a", "b", "a", "b", "c"],
2874
+ # "b" => [1, 2, 1, 3, 3],
2875
+ # "c" => [5, 4, 3, 2, 1]
1729
2876
  # }
1730
2877
  # )
1731
- # df.group_by("a").agg(Polars.col("b").sum).sort("a")
2878
+ # df.group_by("a").agg(Polars.col("b").sum)
1732
2879
  # # =>
1733
2880
  # # shape: (3, 2)
1734
2881
  # # ┌─────┬─────┐
@@ -1736,27 +2883,76 @@ module Polars
1736
2883
  # # │ --- ┆ --- │
1737
2884
  # # │ str ┆ i64 │
1738
2885
  # # ╞═════╪═════╡
1739
- # # │ a ┆ 4
1740
- # # │ b ┆ 11
1741
- # # │ c ┆ 6
2886
+ # # │ a ┆ 2
2887
+ # # │ b ┆ 5
2888
+ # # │ c ┆ 3
1742
2889
  # # └─────┴─────┘
1743
- def group_by(by, maintain_order: false)
1744
- if !Utils.bool?(maintain_order)
1745
- raise TypeError, "invalid input for group_by arg `maintain_order`: #{maintain_order}."
2890
+ #
2891
+ # @example Set `maintain_order: true` to ensure the order of the groups is consistent with the input.
2892
+ # df.group_by("a", maintain_order: true).agg(Polars.col("c"))
2893
+ # # =>
2894
+ # # shape: (3, 2)
2895
+ # # ┌─────┬───────────┐
2896
+ # # │ a ┆ c │
2897
+ # # │ --- ┆ --- │
2898
+ # # │ str ┆ list[i64] │
2899
+ # # ╞═════╪═══════════╡
2900
+ # # │ a ┆ [5, 3] │
2901
+ # # │ b ┆ [4, 2] │
2902
+ # # │ c ┆ [1] │
2903
+ # # └─────┴───────────┘
2904
+ #
2905
+ # @example Group by multiple columns by passing a list of column names.
2906
+ # df.group_by(["a", "b"]).agg(Polars.max("c"))
2907
+ # # =>
2908
+ # # shape: (4, 3)
2909
+ # # ┌─────┬─────┬─────┐
2910
+ # # │ a ┆ b ┆ c │
2911
+ # # │ --- ┆ --- ┆ --- │
2912
+ # # │ str ┆ i64 ┆ i64 │
2913
+ # # ╞═════╪═════╪═════╡
2914
+ # # │ a ┆ 1 ┆ 5 │
2915
+ # # │ b ┆ 2 ┆ 4 │
2916
+ # # │ b ┆ 3 ┆ 2 │
2917
+ # # │ c ┆ 3 ┆ 1 │
2918
+ # # └─────┴─────┴─────┘
2919
+ #
2920
+ # @example Or use positional arguments to group by multiple columns in the same way. Expressions are also accepted.
2921
+ # df.group_by("a", Polars.col("b") / 2).agg(Polars.col("c").mean)
2922
+ # # =>
2923
+ # # shape: (3, 3)
2924
+ # # ┌─────┬─────┬─────┐
2925
+ # # │ a ┆ b ┆ c │
2926
+ # # │ --- ┆ --- ┆ --- │
2927
+ # # │ str ┆ i64 ┆ f64 │
2928
+ # # ╞═════╪═════╪═════╡
2929
+ # # │ a ┆ 0 ┆ 4.0 │
2930
+ # # │ b ┆ 1 ┆ 3.0 │
2931
+ # # │ c ┆ 1 ┆ 1.0 │
2932
+ # # └─────┴─────┴─────┘
2933
+ #
2934
+ # @example The `GroupBy` object returned by this method is iterable, returning the name and data of each group.
2935
+ # df.group_by("a").each do |name, data|
2936
+ # # ...
2937
+ # end
2938
+ def group_by(*by, maintain_order: false, **named_by)
2939
+ named_by.each do |_, value|
2940
+ if !(value.is_a?(::String) || value.is_a?(Expr) || value.is_a?(Series))
2941
+ msg = "Expected Polars expression or object convertible to one, got #{value.class.name}."
2942
+ raise TypeError, msg
2943
+ end
1746
2944
  end
1747
2945
  GroupBy.new(
1748
2946
  self,
1749
- by,
1750
- maintain_order: maintain_order
2947
+ *by,
2948
+ **named_by,
2949
+ maintain_order: maintain_order,
2950
+ predicates: nil
1751
2951
  )
1752
2952
  end
1753
- alias_method :groupby, :group_by
1754
- alias_method :group, :group_by
1755
2953
 
1756
2954
  # Create rolling groups based on a time column.
1757
2955
  #
1758
- # Also works for index values of type `:i32` or `:i64`.
1759
- #
1760
2956
  # Different from a `dynamic_group_by` the windows are now determined by the
1761
2957
  # individual values and are not of constant intervals. For constant intervals use
1762
2958
  # *group_by_dynamic*
@@ -1790,16 +2986,16 @@ module Polars
1790
2986
  # This column must be sorted in ascending order. If not the output will not
1791
2987
  # make sense.
1792
2988
  #
1793
- # In case of a rolling group by on indices, dtype needs to be one of
1794
- # `:i32`, `:i64`. Note that `:i32` gets temporarily cast to `:i64`, so if
1795
- # performance matters use an `:i64` column.
2989
+ # In case of a rolling operation on indices, dtype needs to be one of
2990
+ # \\\\{UInt32, UInt64, Int32, Int64}. Note that the first three get temporarily
2991
+ # cast to Int64, so if performance matters use an Int64 column.
1796
2992
  # @param period [Object]
1797
2993
  # Length of the window.
1798
2994
  # @param offset [Object]
1799
2995
  # Offset of the window. Default is -period.
1800
2996
  # @param closed ["right", "left", "both", "none"]
1801
2997
  # Define whether the temporal window interval is closed or not.
1802
- # @param by [Object]
2998
+ # @param group_by [Object]
1803
2999
  # Also group by this column/these columns.
1804
3000
  #
1805
3001
  # @return [RollingGroupBy]
@@ -1813,7 +3009,7 @@ module Polars
1813
3009
  # "2020-01-03 19:45:32",
1814
3010
  # "2020-01-08 23:16:43"
1815
3011
  # ]
1816
- # df = Polars::DataFrame.new({"dt" => dates, "a" => [3, 7, 5, 9, 2, 1]}).with_column(
3012
+ # df = Polars::DataFrame.new({"dt" => dates, "a" => [3, 7, 5, 9, 2, 1]}).with_columns(
1817
3013
  # Polars.col("dt").str.strptime(Polars::Datetime).set_sorted
1818
3014
  # )
1819
3015
  # df.rolling(index_column: "dt", period: "2d").agg(
@@ -1842,14 +3038,12 @@ module Polars
1842
3038
  period:,
1843
3039
  offset: nil,
1844
3040
  closed: "right",
1845
- by: nil
3041
+ group_by: nil
1846
3042
  )
1847
- RollingGroupBy.new(self, index_column, period, offset, closed, by)
3043
+ RollingGroupBy.new(self, index_column, period, offset, closed, group_by, nil)
1848
3044
  end
1849
- alias_method :groupby_rolling, :rolling
1850
- alias_method :group_by_rolling, :rolling
1851
3045
 
1852
- # Group based on a time value (or index value of type `:i32`, `:i64`).
3046
+ # Group based on a time value (or index value of type Int32, Int64).
1853
3047
  #
1854
3048
  # Time windows are calculated and rows are assigned to windows. Different from a
1855
3049
  # normal group by is that a row can be member of multiple groups. The time/index
@@ -1892,25 +3086,47 @@ module Polars
1892
3086
  # make sense.
1893
3087
  #
1894
3088
  # In case of a dynamic group by on indices, dtype needs to be one of
1895
- # `:i32`, `:i64`. Note that `:i32` gets temporarily cast to `:i64`, so if
1896
- # performance matters use an `:i64` column.
3089
+ # \\\\{Int32, Int64}. Note that Int32 gets temporarily cast to Int64, so if
3090
+ # performance matters use an Int64 column.
1897
3091
  # @param every
1898
3092
  # Interval of the window.
1899
3093
  # @param period
1900
- # Length of the window, if None it is equal to 'every'.
3094
+ # Length of the window, if nil it is equal to 'every'.
1901
3095
  # @param offset
1902
- # Offset of the window if None and period is None it will be equal to negative
3096
+ # Offset of the window if nil and period is nil it will be equal to negative
1903
3097
  # `every`.
1904
- # @param truncate
1905
- # Truncate the time value to the window lower bound.
1906
3098
  # @param include_boundaries
1907
3099
  # Add the lower and upper bound of the window to the "_lower_bound" and
1908
3100
  # "_upper_bound" columns. This will impact performance because it's harder to
1909
3101
  # parallelize
1910
3102
  # @param closed ["right", "left", "both", "none"]
1911
3103
  # Define whether the temporal window interval is closed or not.
1912
- # @param by
3104
+ # @param label ['left', 'right', 'datapoint']
3105
+ # Define which label to use for the window:
3106
+ #
3107
+ # - 'left': lower boundary of the window
3108
+ # - 'right': upper boundary of the window
3109
+ # - 'datapoint': the first value of the index column in the given window.
3110
+ # If you don't need the label to be at one of the boundaries, choose this
3111
+ # option for maximum performance
3112
+ # @param group_by
1913
3113
  # Also group by this column/these columns
3114
+ # @param start_by ['window', 'datapoint', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
3115
+ # The strategy to determine the start of the first window by.
3116
+ #
3117
+ # * 'window': Start by taking the earliest timestamp, truncating it with
3118
+ # `every`, and then adding `offset`.
3119
+ # Note that weekly windows start on Monday.
3120
+ # * 'datapoint': Start from the first encountered data point.
3121
+ # * a day of the week (only takes effect if `every` contains `'w'`):
3122
+ #
3123
+ # * 'monday': Start the window on the Monday before the first data point.
3124
+ # * 'tuesday': Start the window on the Tuesday before the first data point.
3125
+ # * ...
3126
+ # * 'sunday': Start the window on the Sunday before the first data point.
3127
+ #
3128
+ # The resulting window is then shifted back until the earliest datapoint
3129
+ # is in or in front of it.
1914
3130
  #
1915
3131
  # @return [DataFrame]
1916
3132
  #
@@ -2035,7 +3251,7 @@ module Polars
2035
3251
  # "time",
2036
3252
  # every: "1h",
2037
3253
  # closed: "both",
2038
- # by: "groups",
3254
+ # group_by: "groups",
2039
3255
  # include_boundaries: true
2040
3256
  # ).agg([Polars.col("time").count.alias("time_count")])
2041
3257
  # # =>
@@ -2085,10 +3301,10 @@ module Polars
2085
3301
  every:,
2086
3302
  period: nil,
2087
3303
  offset: nil,
2088
- truncate: true,
2089
3304
  include_boundaries: false,
2090
3305
  closed: "left",
2091
- by: nil,
3306
+ label: "left",
3307
+ group_by: nil,
2092
3308
  start_by: "window"
2093
3309
  )
2094
3310
  DynamicGroupBy.new(
@@ -2097,14 +3313,14 @@ module Polars
2097
3313
  every,
2098
3314
  period,
2099
3315
  offset,
2100
- truncate,
2101
3316
  include_boundaries,
2102
3317
  closed,
2103
- by,
2104
- start_by
3318
+ label,
3319
+ group_by,
3320
+ start_by,
3321
+ nil
2105
3322
  )
2106
3323
  end
2107
- alias_method :groupby_dynamic, :group_by_dynamic
2108
3324
 
2109
3325
  # Upsample a DataFrame at a regular frequency.
2110
3326
  #
@@ -2113,7 +3329,7 @@ module Polars
2113
3329
  # Note that this column has to be sorted for the output to make sense.
2114
3330
  # @param every [String]
2115
3331
  # interval will start 'every' duration
2116
- # @param by [Object]
3332
+ # @param group_by [Object]
2117
3333
  # First group by these columns and then upsample for every group
2118
3334
  # @param maintain_order [Boolean]
2119
3335
  # Keep the ordering predictable. This is slower.
@@ -2152,7 +3368,7 @@ module Polars
2152
3368
  # }
2153
3369
  # ).set_sorted("time")
2154
3370
  # df.upsample(
2155
- # time_column: "time", every: "1mo", by: "groups", maintain_order: true
3371
+ # time_column: "time", every: "1mo", group_by: "groups", maintain_order: true
2156
3372
  # ).select(Polars.all.forward_fill)
2157
3373
  # # =>
2158
3374
  # # shape: (7, 3)
@@ -2172,20 +3388,20 @@ module Polars
2172
3388
  def upsample(
2173
3389
  time_column:,
2174
3390
  every:,
2175
- by: nil,
3391
+ group_by: nil,
2176
3392
  maintain_order: false
2177
3393
  )
2178
- if by.nil?
2179
- by = []
3394
+ if group_by.nil?
3395
+ group_by = []
2180
3396
  end
2181
- if by.is_a?(::String)
2182
- by = [by]
3397
+ if group_by.is_a?(::String)
3398
+ group_by = [group_by]
2183
3399
  end
2184
3400
 
2185
3401
  every = Utils.parse_as_duration_string(every)
2186
3402
 
2187
3403
  _from_rbdf(
2188
- _df.upsample(by, time_column, every, maintain_order)
3404
+ _df.upsample(group_by, time_column, every, maintain_order)
2189
3405
  )
2190
3406
  end
2191
3407
 
@@ -2211,14 +3427,14 @@ module Polars
2211
3427
  # Join column of the right DataFrame.
2212
3428
  # @param on [String]
2213
3429
  # Join column of both DataFrames. If set, `left_on` and `right_on` should be
2214
- # None.
2215
- # @param by [Object]
2216
- # join on these columns before doing asof join
3430
+ # nil.
2217
3431
  # @param by_left [Object]
2218
3432
  # join on these columns before doing asof join
2219
3433
  # @param by_right [Object]
2220
3434
  # join on these columns before doing asof join
2221
- # @param strategy ["backward", "forward"]
3435
+ # @param by [Object]
3436
+ # join on these columns before doing asof join
3437
+ # @param strategy ["backward", "forward"]
2222
3438
  # Join strategy.
2223
3439
  # @param suffix [String]
2224
3440
  # Suffix to append to columns with a duplicate name.
@@ -2254,6 +3470,14 @@ module Polars
2254
3470
  # - true: -> Always coalesce join columns.
2255
3471
  # - false: -> Never coalesce join columns.
2256
3472
  # Note that joining on any other expressions than `col` will turn off coalescing.
3473
+ # @param allow_exact_matches [Boolean]
3474
+ # Whether exact matches are valid join predicates.
3475
+ # - If true, allow matching with the same `on` value (i.e. less-than-or-equal-to / greater-than-or-equal-to).
3476
+ # - If false, don't match the same `on` value (i.e., strictly less-than / strictly greater-than).
3477
+ # @param check_sortedness [Boolean]
3478
+ # Check the sortedness of the asof keys. If the keys are not sorted Polars
3479
+ # will error, or in case of 'by' argument raise a warning. This might become
3480
+ # a hard error in the future.
2257
3481
  #
2258
3482
  # @return [DataFrame]
2259
3483
  #
@@ -2308,7 +3532,9 @@ module Polars
2308
3532
  tolerance: nil,
2309
3533
  allow_parallel: true,
2310
3534
  force_parallel: false,
2311
- coalesce: true
3535
+ coalesce: true,
3536
+ allow_exact_matches: true,
3537
+ check_sortedness: true
2312
3538
  )
2313
3539
  lazy
2314
3540
  .join_asof(
@@ -2324,9 +3550,11 @@ module Polars
2324
3550
  tolerance: tolerance,
2325
3551
  allow_parallel: allow_parallel,
2326
3552
  force_parallel: force_parallel,
2327
- coalesce: coalesce
3553
+ coalesce: coalesce,
3554
+ allow_exact_matches: allow_exact_matches,
3555
+ check_sortedness: check_sortedness
2328
3556
  )
2329
- .collect(no_optimization: true)
3557
+ .collect(optimizations: QueryOptFlags._eager)
2330
3558
  end
2331
3559
 
2332
3560
  # Join in SQL-like fashion.
@@ -2349,7 +3577,7 @@ module Polars
2349
3577
  # * *one_to_one* - “1:1”: check if join keys are unique in both left and right datasets
2350
3578
  # * *one_to_many* - “1:m”: check if join keys are unique in left dataset
2351
3579
  # * *many_to_one* - “m:1”: check if join keys are unique in right dataset
2352
- # @param join_nulls [Boolean]
3580
+ # @param nulls_equal [Boolean]
2353
3581
  # Join on null values. By default null values will never produce matches.
2354
3582
  # @param coalesce [Boolean]
2355
3583
  # Coalescing behavior (merging of join columns).
@@ -2357,6 +3585,24 @@ module Polars
2357
3585
  # - true: -> Always coalesce join columns.
2358
3586
  # - false: -> Never coalesce join columns.
2359
3587
  # Note that joining on any other expressions than `col` will turn off coalescing.
3588
+ # @param maintain_order ['none', 'left', 'right', 'left_right', 'right_left']
3589
+ # Which DataFrame row order to preserve, if any.
3590
+ # Do not rely on any observed ordering without explicitly
3591
+ # setting this parameter, as your code may break in a future release.
3592
+ # Not specifying any ordering can improve performance
3593
+ # Supported for inner, left, right and full joins
3594
+ #
3595
+ # * *none*
3596
+ # No specific ordering is desired. The ordering might differ across
3597
+ # Polars versions or even between different runs.
3598
+ # * *left*
3599
+ # Preserves the order of the left DataFrame.
3600
+ # * *right*
3601
+ # Preserves the order of the right DataFrame.
3602
+ # * *left_right*
3603
+ # First preserves the order of the left DataFrame, then the right.
3604
+ # * *right_left*
3605
+ # First preserves the order of the right DataFrame, then the left.
2360
3606
  #
2361
3607
  # @return [DataFrame]
2362
3608
  #
@@ -2439,15 +3685,17 @@ module Polars
2439
3685
  # # ╞═════╪═════╪═════╡
2440
3686
  # # │ 3 ┆ 8.0 ┆ c │
2441
3687
  # # └─────┴─────┴─────┘
2442
- def join(other,
3688
+ def join(
3689
+ other,
2443
3690
  left_on: nil,
2444
3691
  right_on: nil,
2445
3692
  on: nil,
2446
3693
  how: "inner",
2447
3694
  suffix: "_right",
2448
3695
  validate: "m:m",
2449
- join_nulls: false,
2450
- coalesce: nil
3696
+ nulls_equal: false,
3697
+ coalesce: nil,
3698
+ maintain_order: nil
2451
3699
  )
2452
3700
  lazy
2453
3701
  .join(
@@ -2458,10 +3706,157 @@ module Polars
2458
3706
  how: how,
2459
3707
  suffix: suffix,
2460
3708
  validate: validate,
2461
- join_nulls: join_nulls,
2462
- coalesce: coalesce
3709
+ nulls_equal: nulls_equal,
3710
+ coalesce: coalesce,
3711
+ maintain_order: maintain_order
2463
3712
  )
2464
- .collect(no_optimization: true)
3713
+ .collect(optimizations: QueryOptFlags._eager)
3714
+ end
3715
+
3716
+ # Perform a join based on one or multiple (in)equality predicates.
3717
+ #
3718
+ # This performs an inner join, so only rows where all predicates are true
3719
+ # are included in the result, and a row from either DataFrame may be included
3720
+ # multiple times in the result.
3721
+ #
3722
+ # @note
3723
+ # The row order of the input DataFrames is not preserved.
3724
+ #
3725
+ # @note
3726
+ # This functionality is experimental. It may be
3727
+ # changed at any point without it being considered a breaking change.
3728
+ #
3729
+ # @param other [DataFrame]
3730
+ # DataFrame to join with.
3731
+ # @param predicates [Array]
3732
+ # (In)Equality condition to join the two tables on.
3733
+ # When a column name occurs in both tables, the proper suffix must
3734
+ # be applied in the predicate.
3735
+ # @param suffix [String]
3736
+ # Suffix to append to columns with a duplicate name.
3737
+ #
3738
+ # @return [DataFrame]
3739
+ #
3740
+ # @example Join two dataframes together based on two predicates which get AND-ed together.
3741
+ # east = Polars::DataFrame.new(
3742
+ # {
3743
+ # "id": [100, 101, 102],
3744
+ # "dur": [120, 140, 160],
3745
+ # "rev": [12, 14, 16],
3746
+ # "cores": [2, 8, 4]
3747
+ # }
3748
+ # )
3749
+ # west = Polars::DataFrame.new(
3750
+ # {
3751
+ # "t_id": [404, 498, 676, 742],
3752
+ # "time": [90, 130, 150, 170],
3753
+ # "cost": [9, 13, 15, 16],
3754
+ # "cores": [4, 2, 1, 4]
3755
+ # }
3756
+ # )
3757
+ # east.join_where(
3758
+ # west,
3759
+ # Polars.col("dur") < Polars.col("time"),
3760
+ # Polars.col("rev") < Polars.col("cost")
3761
+ # )
3762
+ # # =>
3763
+ # # shape: (5, 8)
3764
+ # # ┌─────┬─────┬─────┬───────┬──────┬──────┬──────┬─────────────┐
3765
+ # # │ id ┆ dur ┆ rev ┆ cores ┆ t_id ┆ time ┆ cost ┆ cores_right │
3766
+ # # │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
3767
+ # # │ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 │
3768
+ # # ╞═════╪═════╪═════╪═══════╪══════╪══════╪══════╪═════════════╡
3769
+ # # │ 100 ┆ 120 ┆ 12 ┆ 2 ┆ 498 ┆ 130 ┆ 13 ┆ 2 │
3770
+ # # │ 100 ┆ 120 ┆ 12 ┆ 2 ┆ 676 ┆ 150 ┆ 15 ┆ 1 │
3771
+ # # │ 100 ┆ 120 ┆ 12 ┆ 2 ┆ 742 ┆ 170 ┆ 16 ┆ 4 │
3772
+ # # │ 101 ┆ 140 ┆ 14 ┆ 8 ┆ 676 ┆ 150 ┆ 15 ┆ 1 │
3773
+ # # │ 101 ┆ 140 ┆ 14 ┆ 8 ┆ 742 ┆ 170 ┆ 16 ┆ 4 │
3774
+ # # └─────┴─────┴─────┴───────┴──────┴──────┴──────┴─────────────┘
3775
+ #
3776
+ # @example To OR them together, use a single expression and the `|` operator.
3777
+ # east.join_where(
3778
+ # west,
3779
+ # (Polars.col("dur") < Polars.col("time")) | (Polars.col("rev") < Polars.col("cost"))
3780
+ # )
3781
+ # # =>
3782
+ # # shape: (6, 8)
3783
+ # # ┌─────┬─────┬─────┬───────┬──────┬──────┬──────┬─────────────┐
3784
+ # # │ id ┆ dur ┆ rev ┆ cores ┆ t_id ┆ time ┆ cost ┆ cores_right │
3785
+ # # │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
3786
+ # # │ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 │
3787
+ # # ╞═════╪═════╪═════╪═══════╪══════╪══════╪══════╪═════════════╡
3788
+ # # │ 100 ┆ 120 ┆ 12 ┆ 2 ┆ 498 ┆ 130 ┆ 13 ┆ 2 │
3789
+ # # │ 100 ┆ 120 ┆ 12 ┆ 2 ┆ 676 ┆ 150 ┆ 15 ┆ 1 │
3790
+ # # │ 100 ┆ 120 ┆ 12 ┆ 2 ┆ 742 ┆ 170 ┆ 16 ┆ 4 │
3791
+ # # │ 101 ┆ 140 ┆ 14 ┆ 8 ┆ 676 ┆ 150 ┆ 15 ┆ 1 │
3792
+ # # │ 101 ┆ 140 ┆ 14 ┆ 8 ┆ 742 ┆ 170 ┆ 16 ┆ 4 │
3793
+ # # │ 102 ┆ 160 ┆ 16 ┆ 4 ┆ 742 ┆ 170 ┆ 16 ┆ 4 │
3794
+ # # └─────┴─────┴─────┴───────┴──────┴──────┴──────┴─────────────┘
3795
+ def join_where(
3796
+ other,
3797
+ *predicates,
3798
+ suffix: "_right"
3799
+ )
3800
+ Utils.require_same_type(self, other)
3801
+
3802
+ lazy
3803
+ .join_where(
3804
+ other.lazy,
3805
+ *predicates,
3806
+ suffix: suffix
3807
+ )
3808
+ .collect(optimizations: QueryOptFlags._eager)
3809
+ end
3810
+
3811
+ # Selects rows from this DataFrame at the given indices.
3812
+ #
3813
+ # @note
3814
+ # This functionality is experimental. It may be
3815
+ # changed at any point without it being considered a breaking change.
3816
+ #
3817
+ # @param indices [Object]
3818
+ # The indices of the rows to select.
3819
+ # @param null_on_oob [Boolean]
3820
+ # If true when an index is out-of-bounds a null row will be generated
3821
+ # instead of raising an error.
3822
+ #
3823
+ # @return [DataFrame]
3824
+ #
3825
+ # @example
3826
+ # df = Polars::DataFrame.new({"x" => [2, 1, 0], "s" => ["foo", "bar", "baz"]})
3827
+ # df.gather([2, 0, 0])
3828
+ # # =>
3829
+ # # shape: (3, 2)
3830
+ # # ┌─────┬─────┐
3831
+ # # │ x ┆ s │
3832
+ # # │ --- ┆ --- │
3833
+ # # │ i64 ┆ str │
3834
+ # # ╞═════╪═════╡
3835
+ # # │ 0 ┆ baz │
3836
+ # # │ 2 ┆ foo │
3837
+ # # │ 2 ┆ foo │
3838
+ # # └─────┴─────┘
3839
+ #
3840
+ # @example
3841
+ # df.gather([0, 10, 1], null_on_oob: true)
3842
+ # # =>
3843
+ # # shape: (3, 2)
3844
+ # # ┌──────┬──────┐
3845
+ # # │ x ┆ s │
3846
+ # # │ --- ┆ --- │
3847
+ # # │ i64 ┆ str │
3848
+ # # ╞══════╪══════╡
3849
+ # # │ 2 ┆ foo │
3850
+ # # │ null ┆ null │
3851
+ # # │ 1 ┆ bar │
3852
+ # # └──────┴──────┘
3853
+ def gather(
3854
+ indices,
3855
+ null_on_oob: false
3856
+ )
3857
+ lazy
3858
+ .gather(indices, null_on_oob: null_on_oob)
3859
+ .collect(optimizations: QueryOptFlags._eager)
2465
3860
  end
2466
3861
 
2467
3862
  # Apply a custom/user-defined function (UDF) over the rows of the DataFrame.
@@ -2524,61 +3919,14 @@ module Polars
2524
3919
  # # │ 9 │
2525
3920
  # # │ 14 │
2526
3921
  # # └─────┘
2527
- def map_rows(return_dtype: nil, inference_size: 256, &f)
2528
- out, is_df = _df.map_rows(f, return_dtype, inference_size)
3922
+ def map_rows(return_dtype: nil, inference_size: 256, &function)
3923
+ out, is_df = _df.map_rows(function, return_dtype, inference_size)
2529
3924
  if is_df
2530
3925
  _from_rbdf(out)
2531
3926
  else
2532
3927
  _from_rbdf(Utils.wrap_s(out).to_frame._df)
2533
3928
  end
2534
3929
  end
2535
- alias_method :apply, :map_rows
2536
-
2537
- # Return a new DataFrame with the column added or replaced.
2538
- #
2539
- # @param column [Object]
2540
- # Series, where the name of the Series refers to the column in the DataFrame.
2541
- #
2542
- # @return [DataFrame]
2543
- #
2544
- # @example Added
2545
- # df = Polars::DataFrame.new(
2546
- # {
2547
- # "a" => [1, 3, 5],
2548
- # "b" => [2, 4, 6]
2549
- # }
2550
- # )
2551
- # df.with_column((Polars.col("b") ** 2).alias("b_squared"))
2552
- # # =>
2553
- # # shape: (3, 3)
2554
- # # ┌─────┬─────┬───────────┐
2555
- # # │ a ┆ b ┆ b_squared │
2556
- # # │ --- ┆ --- ┆ --- │
2557
- # # │ i64 ┆ i64 ┆ i64 │
2558
- # # ╞═════╪═════╪═══════════╡
2559
- # # │ 1 ┆ 2 ┆ 4 │
2560
- # # │ 3 ┆ 4 ┆ 16 │
2561
- # # │ 5 ┆ 6 ┆ 36 │
2562
- # # └─────┴─────┴───────────┘
2563
- #
2564
- # @example Replaced
2565
- # df.with_column(Polars.col("a") ** 2)
2566
- # # =>
2567
- # # shape: (3, 2)
2568
- # # ┌─────┬─────┐
2569
- # # │ a ┆ b │
2570
- # # │ --- ┆ --- │
2571
- # # │ i64 ┆ i64 │
2572
- # # ╞═════╪═════╡
2573
- # # │ 1 ┆ 2 │
2574
- # # │ 9 ┆ 4 │
2575
- # # │ 25 ┆ 6 │
2576
- # # └─────┴─────┘
2577
- def with_column(column)
2578
- lazy
2579
- .with_column(column)
2580
- .collect(no_optimization: true, string_cache: false)
2581
- end
2582
3930
 
2583
3931
  # Return a new DataFrame grown horizontally by stacking multiple Series to it.
2584
3932
  #
@@ -2624,7 +3972,7 @@ module Polars
2624
3972
 
2625
3973
  # Grow this DataFrame vertically by stacking a DataFrame to it.
2626
3974
  #
2627
- # @param df [DataFrame]
3975
+ # @param other [DataFrame]
2628
3976
  # DataFrame to stack.
2629
3977
  # @param in_place [Boolean]
2630
3978
  # Modify in place
@@ -2659,12 +4007,12 @@ module Polars
2659
4007
  # # │ 3 ┆ 8 ┆ c │
2660
4008
  # # │ 4 ┆ 9 ┆ d │
2661
4009
  # # └─────┴─────┴─────┘
2662
- def vstack(df, in_place: false)
4010
+ def vstack(other, in_place: false)
2663
4011
  if in_place
2664
- _df.vstack_mut(df._df)
4012
+ _df.vstack_mut(other._df)
2665
4013
  self
2666
4014
  else
2667
- _from_rbdf(_df.vstack(df._df))
4015
+ _from_rbdf(_df.vstack(other._df))
2668
4016
  end
2669
4017
  end
2670
4018
 
@@ -2717,6 +4065,9 @@ module Polars
2717
4065
  #
2718
4066
  # @param columns [Object]
2719
4067
  # Column(s) to drop.
4068
+ # @param strict [Boolean]
4069
+ # Validate that all column names exist in the current schema,
4070
+ # and throw an exception if any do not.
2720
4071
  #
2721
4072
  # @return [DataFrame]
2722
4073
  #
@@ -2768,8 +4119,8 @@ module Polars
2768
4119
  # # │ 7.0 │
2769
4120
  # # │ 8.0 │
2770
4121
  # # └─────┘
2771
- def drop(*columns)
2772
- lazy.drop(*columns).collect(_eager: true)
4122
+ def drop(*columns, strict: true)
4123
+ lazy.drop(*columns, strict: strict).collect(optimizations: QueryOptFlags._eager)
2773
4124
  end
2774
4125
 
2775
4126
  # Drop in place.
@@ -2882,7 +4233,7 @@ module Polars
2882
4233
  # df.cast(Polars::String).to_h(as_series: false)
2883
4234
  # # => {"foo"=>["1", "2", "3"], "bar"=>["6.0", "7.0", "8.0"], "ham"=>["2020-01-02", "2021-03-04", "2022-05-06"]}
2884
4235
  def cast(dtypes, strict: true)
2885
- lazy.cast(dtypes, strict: strict).collect(_eager: true)
4236
+ lazy.cast(dtypes, strict: strict).collect(optimizations: QueryOptFlags._eager)
2886
4237
  end
2887
4238
 
2888
4239
  # Create an empty copy of the current DataFrame.
@@ -2932,7 +4283,6 @@ module Polars
2932
4283
  clone
2933
4284
  end
2934
4285
  end
2935
- alias_method :cleared, :clear
2936
4286
 
2937
4287
  # clone handled by initialize_copy
2938
4288
 
@@ -2994,10 +4344,13 @@ module Polars
2994
4344
  _df.get_columns.map { |s| Utils.wrap_s(s) }
2995
4345
  end
2996
4346
 
2997
- # Get a single column as Series by name.
4347
+ # Get a single column by name.
2998
4348
  #
2999
4349
  # @param name [String]
3000
4350
  # Name of the column to retrieve.
4351
+ # @param default [Object]
4352
+ # Value to return if the column does not exist; if not explicitly set and
4353
+ # the column is not present a `ColumnNotFoundError` exception is raised.
3001
4354
  #
3002
4355
  # @return [Series]
3003
4356
  #
@@ -3012,8 +4365,22 @@ module Polars
3012
4365
  # # 2
3013
4366
  # # 3
3014
4367
  # # ]
3015
- def get_column(name)
3016
- self[name]
4368
+ #
4369
+ # @example
4370
+ # df.get_column("baz", default: Polars::Series.new("baz", ["?", "?", "?"]))
4371
+ # # =>
4372
+ # # shape: (3,)
4373
+ # # Series: 'baz' [str]
4374
+ # # [
4375
+ # # "?"
4376
+ # # "?"
4377
+ # # "?"
4378
+ # # ]
4379
+ def get_column(name, default: NO_DEFAULT)
4380
+ Utils.wrap_s(_df.get_column(name.to_s))
4381
+ rescue ColumnNotFoundError
4382
+ raise if default.eql?(NO_DEFAULT)
4383
+ default
3017
4384
  end
3018
4385
 
3019
4386
  # Fill null values using the specified value or strategy.
@@ -3099,14 +4466,14 @@ module Polars
3099
4466
  _from_rbdf(
3100
4467
  lazy
3101
4468
  .fill_null(value, strategy: strategy, limit: limit, matches_supertype: matches_supertype)
3102
- .collect(no_optimization: true)
4469
+ .collect(optimizations: QueryOptFlags._eager)
3103
4470
  ._df
3104
4471
  )
3105
4472
  end
3106
4473
 
3107
4474
  # Fill floating point NaN values by an Expression evaluation.
3108
4475
  #
3109
- # @param fill_value [Object]
4476
+ # @param value [Object]
3110
4477
  # Value to fill NaN with.
3111
4478
  #
3112
4479
  # @return [DataFrame]
@@ -3135,14 +4502,20 @@ module Polars
3135
4502
  # # │ 99.0 ┆ 99.0 │
3136
4503
  # # │ 4.0 ┆ 13.0 │
3137
4504
  # # └──────┴──────┘
3138
- def fill_nan(fill_value)
3139
- lazy.fill_nan(fill_value).collect(no_optimization: true)
4505
+ def fill_nan(value)
4506
+ lazy.fill_nan(value).collect(optimizations: QueryOptFlags._eager)
3140
4507
  end
3141
4508
 
3142
4509
  # Explode `DataFrame` to long format by exploding a column with Lists.
3143
4510
  #
3144
4511
  # @param columns [Object]
3145
4512
  # Column of LargeList type.
4513
+ # @param more_columns [Array]
4514
+ # Additional names of columns to explode, specified as positional arguments.
4515
+ # @param empty_as_null [Boolean]
4516
+ # Explode an empty list/array into a `null`.
4517
+ # @param keep_nulls [Boolean]
4518
+ # Explode a `null` list/array into a `null`.
3146
4519
  #
3147
4520
  # @return [DataFrame]
3148
4521
  #
@@ -3170,25 +4543,43 @@ module Polars
3170
4543
  # # │ c ┆ 7 │
3171
4544
  # # │ c ┆ 8 │
3172
4545
  # # └─────────┴─────────┘
3173
- def explode(columns)
3174
- lazy.explode(columns).collect(no_optimization: true)
4546
+ def explode(
4547
+ columns,
4548
+ *more_columns,
4549
+ empty_as_null: true,
4550
+ keep_nulls: true
4551
+ )
4552
+ lazy
4553
+ .explode(
4554
+ columns,
4555
+ *more_columns,
4556
+ empty_as_null: empty_as_null,
4557
+ keep_nulls: keep_nulls
4558
+ ).collect(optimizations: QueryOptFlags._eager)
3175
4559
  end
3176
4560
 
3177
4561
  # Create a spreadsheet-style pivot table as a DataFrame.
3178
4562
  #
4563
+ # @param on [Object]
4564
+ # Columns whose values will be used as the header of the output DataFrame
4565
+ # @param on_columns [Object]
4566
+ # What value combinations will be considered for the output table.
4567
+ # @param index [Object]
4568
+ # One or multiple keys to group by
3179
4569
  # @param values [Object]
3180
4570
  # Column values to aggregate. Can be multiple columns if the *columns*
3181
4571
  # arguments contains multiple columns as well
3182
- # @param index [Object]
3183
- # One or multiple keys to group by
3184
- # @param on [Object]
3185
- # Columns whose values will be used as the header of the output DataFrame
3186
4572
  # @param aggregate_function ["first", "sum", "max", "min", "mean", "median", "last", "count"]
3187
4573
  # A predefined aggregate function str or an expression.
3188
4574
  # @param maintain_order [Object]
3189
4575
  # Sort the grouped keys so that the output order is predictable.
3190
4576
  # @param sort_columns [Object]
3191
4577
  # Sort the transposed columns by name. Default is by order of discovery.
4578
+ # @param separator [String]
4579
+ # Used as separator/delimiter in generated column names in case of multiple
4580
+ # `values` columns.
4581
+ # @param column_naming ['auto', 'combine']
4582
+ # How resulting column names will be constructed.
3192
4583
  #
3193
4584
  # @return [DataFrame]
3194
4585
  #
@@ -3213,60 +4604,37 @@ module Polars
3213
4604
  # # └─────┴─────┴─────┘
3214
4605
  def pivot(
3215
4606
  on,
4607
+ on_columns: nil,
3216
4608
  index: nil,
3217
4609
  values: nil,
3218
4610
  aggregate_function: nil,
3219
4611
  maintain_order: true,
3220
4612
  sort_columns: false,
3221
- separator: "_"
4613
+ separator: "_",
4614
+ column_naming: "auto"
3222
4615
  )
3223
- index = Utils._expand_selectors(self, index)
3224
- on = Utils._expand_selectors(self, on)
3225
- if !values.nil?
3226
- values = Utils._expand_selectors(self, values)
3227
- end
3228
-
3229
- if aggregate_function.is_a?(::String)
3230
- case aggregate_function
3231
- when "first"
3232
- aggregate_expr = F.element.first._rbexpr
3233
- when "sum"
3234
- aggregate_expr = F.element.sum._rbexpr
3235
- when "max"
3236
- aggregate_expr = F.element.max._rbexpr
3237
- when "min"
3238
- aggregate_expr = F.element.min._rbexpr
3239
- when "mean"
3240
- aggregate_expr = F.element.mean._rbexpr
3241
- when "median"
3242
- aggregate_expr = F.element.median._rbexpr
3243
- when "last"
3244
- aggregate_expr = F.element.last._rbexpr
3245
- when "len"
3246
- aggregate_expr = F.len._rbexpr
3247
- when "count"
3248
- warn "`aggregate_function: \"count\"` input for `pivot` is deprecated. Use `aggregate_function: \"len\"` instead."
3249
- aggregate_expr = F.len._rbexpr
3250
- else
3251
- raise ArgumentError, "Argument aggregate fn: '#{aggregate_fn}' was not expected."
4616
+ if on_columns.nil?
4617
+ cols = select(on).unique(maintain_order: true)
4618
+ if sort_columns
4619
+ cols = cols.sort(on)
3252
4620
  end
3253
- elsif aggregate_function.nil?
3254
- aggregate_expr = nil
4621
+ on_cols = cols
3255
4622
  else
3256
- aggregate_expr = aggregate_function._rbexpr
4623
+ on_cols = on_columns
3257
4624
  end
3258
4625
 
3259
- _from_rbdf(
3260
- _df.pivot_expr(
3261
- on,
3262
- index,
3263
- values,
3264
- maintain_order,
3265
- sort_columns,
3266
- aggregate_expr,
3267
- separator
3268
- )
4626
+ lazy
4627
+ .pivot(
4628
+ on,
4629
+ on_columns: on_cols,
4630
+ index: index,
4631
+ values: values,
4632
+ aggregate_function: aggregate_function,
4633
+ maintain_order: maintain_order,
4634
+ separator: separator,
4635
+ column_naming: column_naming
3269
4636
  )
4637
+ .collect(optimizations: QueryOptFlags._eager)
3270
4638
  end
3271
4639
 
3272
4640
  # Unpivot a DataFrame from wide to long format.
@@ -3280,7 +4648,8 @@ module Polars
3280
4648
  #
3281
4649
  # @param on [Object]
3282
4650
  # Column(s) or selector(s) to use as values variables; if `on`
3283
- # is empty all columns that are not in `index` will be used.
4651
+ # is empty no columns will be used. If set to `nil` (default)
4652
+ # all columns that are not in `index` will be used.
3284
4653
  # @param index [Object]
3285
4654
  # Column(s) or selector(s) to use as identifier variables.
3286
4655
  # @param variable_name [Object]
@@ -3313,13 +4682,12 @@ module Polars
3313
4682
  # # │ y ┆ c ┆ 4 │
3314
4683
  # # │ z ┆ c ┆ 6 │
3315
4684
  # # └─────┴──────────┴───────┘
3316
- def unpivot(on, index: nil, variable_name: nil, value_name: nil)
3317
- on = on.nil? ? [] : Utils._expand_selectors(self, on)
4685
+ def unpivot(on = nil, index: nil, variable_name: nil, value_name: nil)
4686
+ on = on.nil? ? nil : Utils._expand_selectors(self, on)
3318
4687
  index = index.nil? ? [] : Utils._expand_selectors(self, index)
3319
4688
 
3320
4689
  _from_rbdf(_df.unpivot(on, index, value_name, variable_name))
3321
4690
  end
3322
- alias_method :melt, :unpivot
3323
4691
 
3324
4692
  # Unstack a long table to a wide form without doing an aggregation.
3325
4693
  #
@@ -3424,7 +4792,7 @@ module Polars
3424
4792
 
3425
4793
  if how == "horizontal"
3426
4794
  df = (
3427
- df.with_column(
4795
+ df.with_columns(
3428
4796
  (Polars.arange(0, n_cols * n_rows, eager: true) % n_cols).alias(
3429
4797
  "__sort_order"
3430
4798
  )
@@ -3447,14 +4815,18 @@ module Polars
3447
4815
 
3448
4816
  # Split into multiple DataFrames partitioned by groups.
3449
4817
  #
3450
- # @param groups [Object]
4818
+ # @param by [Object]
3451
4819
  # Groups to partition by.
4820
+ # @param more_by [Array]
4821
+ # Additional names of columns to group by, specified as positional arguments.
3452
4822
  # @param maintain_order [Boolean]
3453
4823
  # Keep predictable output order. This is slower as it requires an extra sort
3454
4824
  # operation.
4825
+ # @param include_key [Boolean]
4826
+ # Include the columns used to partition the DataFrame in the output.
3455
4827
  # @param as_dict [Boolean]
3456
- # If true, return the partitions in a dictionary keyed by the distinct group
3457
- # values instead of a list.
4828
+ # If true, return the partitions in a hash keyed by the distinct group
4829
+ # values instead of an array.
3458
4830
  #
3459
4831
  # @return [Object]
3460
4832
  #
@@ -3496,7 +4868,7 @@ module Polars
3496
4868
  # @example
3497
4869
  # df.partition_by("foo", maintain_order: true, as_dict: true)
3498
4870
  # # =>
3499
- # # {"A"=>shape: (2, 3)
4871
+ # # {["A"]=>shape: (2, 3)
3500
4872
  # # ┌─────┬─────┬─────┐
3501
4873
  # # │ foo ┆ N ┆ bar │
3502
4874
  # # │ --- ┆ --- ┆ --- │
@@ -3504,7 +4876,7 @@ module Polars
3504
4876
  # # ╞═════╪═════╪═════╡
3505
4877
  # # │ A ┆ 1 ┆ k │
3506
4878
  # # │ A ┆ 2 ┆ l │
3507
- # # └─────┴─────┴─────┘, "B"=>shape: (2, 3)
4879
+ # # └─────┴─────┴─────┘, ["B"]=>shape: (2, 3)
3508
4880
  # # ┌─────┬─────┬─────┐
3509
4881
  # # │ foo ┆ N ┆ bar │
3510
4882
  # # │ --- ┆ --- ┆ --- │
@@ -3512,7 +4884,7 @@ module Polars
3512
4884
  # # ╞═════╪═════╪═════╡
3513
4885
  # # │ B ┆ 2 ┆ m │
3514
4886
  # # │ B ┆ 4 ┆ m │
3515
- # # └─────┴─────┴─────┘, "C"=>shape: (1, 3)
4887
+ # # └─────┴─────┴─────┘, ["C"]=>shape: (1, 3)
3516
4888
  # # ┌─────┬─────┬─────┐
3517
4889
  # # │ foo ┆ N ┆ bar │
3518
4890
  # # │ --- ┆ --- ┆ --- │
@@ -3520,30 +4892,26 @@ module Polars
3520
4892
  # # ╞═════╪═════╪═════╡
3521
4893
  # # │ C ┆ 2 ┆ l │
3522
4894
  # # └─────┴─────┴─────┘}
3523
- def partition_by(groups, maintain_order: true, include_key: true, as_dict: false)
3524
- if groups.is_a?(::String)
3525
- groups = [groups]
3526
- elsif !groups.is_a?(::Array)
3527
- groups = Array(groups)
3528
- end
4895
+ def partition_by(by, *more_by, maintain_order: true, include_key: true, as_dict: false)
4896
+ by_parsed = Utils._expand_selectors(self, by, *more_by)
4897
+
4898
+ partitions = _df.partition_by(by_parsed, maintain_order, include_key).map { |df| _from_rbdf(df) }
3529
4899
 
3530
4900
  if as_dict
3531
- out = {}
3532
- if groups.length == 1
3533
- _df.partition_by(groups, maintain_order, include_key).each do |df|
3534
- df = _from_rbdf(df)
3535
- out[df[groups][0, 0]] = df
3536
- end
4901
+ if include_key
4902
+ names = partitions.map { |p| p.select(by_parsed).row(0) }
3537
4903
  else
3538
- _df.partition_by(groups, maintain_order, include_key).each do |df|
3539
- df = _from_rbdf(df)
3540
- out[df[groups].row(0)] = df
4904
+ if !maintain_order
4905
+ msg = "cannot use `partition_by` with `maintain_order: false, include_key: false, as_dict: true`"
4906
+ raise ArgumentError, msg
3541
4907
  end
4908
+ names = select(by_parsed).unique(maintain_order: true).rows
3542
4909
  end
3543
- out
3544
- else
3545
- _df.partition_by(groups, maintain_order, include_key).map { |df| _from_rbdf(df) }
4910
+
4911
+ return names.zip(partitions).to_h
3546
4912
  end
4913
+
4914
+ partitions
3547
4915
  end
3548
4916
 
3549
4917
  # Shift values by the given period.
@@ -3589,41 +4957,8 @@ module Polars
3589
4957
  # # │ 3 ┆ 8 ┆ c │
3590
4958
  # # │ null ┆ null ┆ null │
3591
4959
  # # └──────┴──────┴──────┘
3592
- def shift(n, fill_value: nil)
3593
- lazy.shift(n, fill_value: fill_value).collect(_eager: true)
3594
- end
3595
-
3596
- # Shift the values by a given period and fill the resulting null values.
3597
- #
3598
- # @param periods [Integer]
3599
- # Number of places to shift (may be negative).
3600
- # @param fill_value [Object]
3601
- # fill nil values with this value.
3602
- #
3603
- # @return [DataFrame]
3604
- #
3605
- # @example
3606
- # df = Polars::DataFrame.new(
3607
- # {
3608
- # "foo" => [1, 2, 3],
3609
- # "bar" => [6, 7, 8],
3610
- # "ham" => ["a", "b", "c"]
3611
- # }
3612
- # )
3613
- # df.shift_and_fill(1, 0)
3614
- # # =>
3615
- # # shape: (3, 3)
3616
- # # ┌─────┬─────┬─────┐
3617
- # # │ foo ┆ bar ┆ ham │
3618
- # # │ --- ┆ --- ┆ --- │
3619
- # # │ i64 ┆ i64 ┆ str │
3620
- # # ╞═════╪═════╪═════╡
3621
- # # │ 0 ┆ 0 ┆ 0 │
3622
- # # │ 1 ┆ 6 ┆ a │
3623
- # # │ 2 ┆ 7 ┆ b │
3624
- # # └─────┴─────┴─────┘
3625
- def shift_and_fill(periods, fill_value)
3626
- shift(periods, fill_value: fill_value)
4960
+ def shift(n = 1, fill_value: nil)
4961
+ lazy.shift(n, fill_value: fill_value).collect(optimizations: QueryOptFlags._eager)
3627
4962
  end
3628
4963
 
3629
4964
  # Get a mask of all duplicated rows in this DataFrame.
@@ -3679,6 +5014,16 @@ module Polars
3679
5014
  # Start a lazy query from this point.
3680
5015
  #
3681
5016
  # @return [LazyFrame]
5017
+ #
5018
+ # @example
5019
+ # df = Polars::DataFrame.new(
5020
+ # {
5021
+ # "a" => [nil, 2, 3, 4],
5022
+ # "b" => [0.5, nil, 2.5, 13],
5023
+ # "c" => [true, true, false, nil]
5024
+ # }
5025
+ # )
5026
+ # df.lazy
3682
5027
  def lazy
3683
5028
  wrap_ldf(_df.lazy)
3684
5029
  end
@@ -3772,7 +5117,27 @@ module Polars
3772
5117
  # # │ 10 │
3773
5118
  # # └─────────┘
3774
5119
  def select(*exprs, **named_exprs)
3775
- lazy.select(*exprs, **named_exprs).collect(_eager: true)
5120
+ lazy.select(*exprs, **named_exprs).collect(optimizations: QueryOptFlags._eager)
5121
+ end
5122
+
5123
+ # Select columns from this DataFrame.
5124
+ #
5125
+ # This will run all expression sequentially instead of in parallel.
5126
+ # Use this when the work per expression is cheap.
5127
+ #
5128
+ # @param exprs [Array]
5129
+ # Column(s) to select, specified as positional arguments.
5130
+ # Accepts expression input. Strings are parsed as column names,
5131
+ # other non-expression inputs are parsed as literals.
5132
+ # @param named_exprs [Hash]
5133
+ # Additional columns to select, specified as keyword arguments.
5134
+ # The columns will be renamed to the keyword used.
5135
+ #
5136
+ # @return [DataFrame]
5137
+ def select_seq(*exprs, **named_exprs)
5138
+ lazy
5139
+ .select_seq(*exprs, **named_exprs)
5140
+ .collect(optimizations: QueryOptFlags._eager)
3776
5141
  end
3777
5142
 
3778
5143
  # Add columns to this DataFrame.
@@ -3884,7 +5249,32 @@ module Polars
3884
5249
  # # │ 4 ┆ 13.0 ┆ true ┆ 52.0 ┆ false │
3885
5250
  # # └─────┴──────┴───────┴──────┴───────┘
3886
5251
  def with_columns(*exprs, **named_exprs)
3887
- lazy.with_columns(*exprs, **named_exprs).collect(_eager: true)
5252
+ lazy.with_columns(*exprs, **named_exprs).collect(optimizations: QueryOptFlags._eager)
5253
+ end
5254
+
5255
+ # Add columns to this DataFrame.
5256
+ #
5257
+ # Added columns will replace existing columns with the same name.
5258
+ #
5259
+ # This will run all expression sequentially instead of in parallel.
5260
+ # Use this when the work per expression is cheap.
5261
+ #
5262
+ # @param exprs [Array]
5263
+ # Column(s) to add, specified as positional arguments.
5264
+ # Accepts expression input. Strings are parsed as column names, other
5265
+ # non-expression inputs are parsed as literals.
5266
+ # @param named_exprs [Hash]
5267
+ # Additional columns to add, specified as keyword arguments.
5268
+ # The columns will be renamed to the keyword used.
5269
+ #
5270
+ # @return [DataFrame]
5271
+ def with_columns_seq(
5272
+ *exprs,
5273
+ **named_exprs
5274
+ )
5275
+ lazy
5276
+ .with_columns_seq(*exprs, **named_exprs)
5277
+ .collect(optimizations: QueryOptFlags._eager)
3888
5278
  end
3889
5279
 
3890
5280
  # Get number of chunks used by the ChunkedArrays of this DataFrame.
@@ -3939,14 +5329,32 @@ module Polars
3939
5329
  # # ╞═════╪═════╪═════╡
3940
5330
  # # │ 3 ┆ 8 ┆ c │
3941
5331
  # # └─────┴─────┴─────┘
3942
- def max(axis: 0)
3943
- if axis == 0
3944
- lazy.max.collect(_eager: true)
3945
- elsif axis == 1
3946
- Utils.wrap_s(_df.max_horizontal)
3947
- else
3948
- raise ArgumentError, "Axis should be 0 or 1."
3949
- end
5332
+ def max
5333
+ lazy.max.collect(optimizations: QueryOptFlags._eager)
5334
+ end
5335
+
5336
+ # Get the maximum value horizontally across columns.
5337
+ #
5338
+ # @return [Series]
5339
+ #
5340
+ # @example
5341
+ # df = Polars::DataFrame.new(
5342
+ # {
5343
+ # "foo" => [1, 2, 3],
5344
+ # "bar" => [4.0, 5.0, 6.0]
5345
+ # }
5346
+ # )
5347
+ # df.max_horizontal
5348
+ # # =>
5349
+ # # shape: (3,)
5350
+ # # Series: 'max' [f64]
5351
+ # # [
5352
+ # # 4.0
5353
+ # # 5.0
5354
+ # # 6.0
5355
+ # # ]
5356
+ def max_horizontal
5357
+ select(max: F.max_horizontal(F.all)).to_series
3950
5358
  end
3951
5359
 
3952
5360
  # Aggregate the columns of this DataFrame to their minimum value.
@@ -3971,22 +5379,35 @@ module Polars
3971
5379
  # # ╞═════╪═════╪═════╡
3972
5380
  # # │ 1 ┆ 6 ┆ a │
3973
5381
  # # └─────┴─────┴─────┘
3974
- def min(axis: 0)
3975
- if axis == 0
3976
- lazy.min.collect(_eager: true)
3977
- elsif axis == 1
3978
- Utils.wrap_s(_df.min_horizontal)
3979
- else
3980
- raise ArgumentError, "Axis should be 0 or 1."
3981
- end
5382
+ def min
5383
+ lazy.min.collect(optimizations: QueryOptFlags._eager)
3982
5384
  end
3983
5385
 
3984
- # Aggregate the columns of this DataFrame to their sum value.
5386
+ # Get the minimum value horizontally across columns.
3985
5387
  #
3986
- # @param axis [Integer]
3987
- # Either 0 or 1.
3988
- # @param null_strategy ["ignore", "propagate"]
3989
- # This argument is only used if axis == 1.
5388
+ # @return [Series]
5389
+ #
5390
+ # @example
5391
+ # df = Polars::DataFrame.new(
5392
+ # {
5393
+ # "foo" => [1, 2, 3],
5394
+ # "bar" => [4.0, 5.0, 6.0]
5395
+ # }
5396
+ # )
5397
+ # df.min_horizontal
5398
+ # # =>
5399
+ # # shape: (3,)
5400
+ # # Series: 'min' [f64]
5401
+ # # [
5402
+ # # 1.0
5403
+ # # 2.0
5404
+ # # 3.0
5405
+ # # ]
5406
+ def min_horizontal
5407
+ select(min: F.min_horizontal(F.all)).to_series
5408
+ end
5409
+
5410
+ # Aggregate the columns of this DataFrame to their sum value.
3990
5411
  #
3991
5412
  # @return [DataFrame]
3992
5413
  #
@@ -4008,35 +5429,42 @@ module Polars
4008
5429
  # # ╞═════╪═════╪══════╡
4009
5430
  # # │ 6 ┆ 21 ┆ null │
4010
5431
  # # └─────┴─────┴──────┘
5432
+ def sum
5433
+ lazy.sum.collect(optimizations: QueryOptFlags._eager)
5434
+ end
5435
+
5436
+ # Sum all values horizontally across columns.
5437
+ #
5438
+ # @param ignore_nulls [Boolean]
5439
+ # Ignore null values (default).
5440
+ # If set to `false`, any null value in the input will lead to a null output.
5441
+ #
5442
+ # @return [Series]
4011
5443
  #
4012
5444
  # @example
4013
- # df.sum(axis: 1)
5445
+ # df = Polars::DataFrame.new(
5446
+ # {
5447
+ # "foo" => [1, 2, 3],
5448
+ # "bar" => [4.0, 5.0, 6.0]
5449
+ # }
5450
+ # )
5451
+ # df.sum_horizontal
4014
5452
  # # =>
4015
5453
  # # shape: (3,)
4016
- # # Series: 'foo' [str]
5454
+ # # Series: 'sum' [f64]
4017
5455
  # # [
4018
- # # "16a"
4019
- # # "27b"
4020
- # # "38c"
5456
+ # # 5.0
5457
+ # # 7.0
5458
+ # # 9.0
4021
5459
  # # ]
4022
- def sum(axis: 0, null_strategy: "ignore")
4023
- case axis
4024
- when 0
4025
- lazy.sum.collect(_eager: true)
4026
- when 1
4027
- Utils.wrap_s(_df.sum_horizontal(null_strategy))
4028
- else
4029
- raise ArgumentError, "Axis should be 0 or 1."
4030
- end
5460
+ def sum_horizontal(ignore_nulls: true)
5461
+ select(
5462
+ sum: F.sum_horizontal(F.all, ignore_nulls: ignore_nulls)
5463
+ ).to_series
4031
5464
  end
4032
5465
 
4033
5466
  # Aggregate the columns of this DataFrame to their mean value.
4034
5467
  #
4035
- # @param axis [Integer]
4036
- # Either 0 or 1.
4037
- # @param null_strategy ["ignore", "propagate"]
4038
- # This argument is only used if axis == 1.
4039
- #
4040
5468
  # @return [DataFrame]
4041
5469
  #
4042
5470
  # @example
@@ -4057,40 +5485,63 @@ module Polars
4057
5485
  # # ╞═════╪═════╪══════╡
4058
5486
  # # │ 2.0 ┆ 7.0 ┆ null │
4059
5487
  # # └─────┴─────┴──────┘
4060
- def mean(axis: 0, null_strategy: "ignore")
4061
- case axis
4062
- when 0
4063
- lazy.mean.collect(_eager: true)
4064
- when 1
4065
- Utils.wrap_s(_df.mean_horizontal(null_strategy))
4066
- else
4067
- raise ArgumentError, "Axis should be 0 or 1."
4068
- end
5488
+ def mean
5489
+ lazy.mean.collect(optimizations: QueryOptFlags._eager)
4069
5490
  end
4070
5491
 
4071
- # Aggregate the columns of this DataFrame to their standard deviation value.
5492
+ # Take the mean of all values horizontally across columns.
4072
5493
  #
4073
- # @param ddof [Integer]
4074
- # Degrees of freedom
5494
+ # @param ignore_nulls [Boolean]
5495
+ # Ignore null values (default).
5496
+ # If set to `false`, any null value in the input will lead to a null output.
4075
5497
  #
4076
- # @return [DataFrame]
5498
+ # @return [Series]
4077
5499
  #
4078
5500
  # @example
4079
5501
  # df = Polars::DataFrame.new(
4080
5502
  # {
4081
5503
  # "foo" => [1, 2, 3],
4082
- # "bar" => [6, 7, 8],
4083
- # "ham" => ["a", "b", "c"]
5504
+ # "bar" => [4.0, 5.0, 6.0]
4084
5505
  # }
4085
5506
  # )
4086
- # df.std
5507
+ # df.mean_horizontal
4087
5508
  # # =>
4088
- # # shape: (1, 3)
4089
- # # ┌─────┬─────┬──────┐
4090
- # # │ foo ┆ bar ┆ ham │
4091
- # # │ --- ┆ --- ┆ --- │
4092
- # # │ f64 ┆ f64 ┆ str │
4093
- # # ╞═════╪═════╪══════╡
5509
+ # # shape: (3,)
5510
+ # # Series: 'mean' [f64]
5511
+ # # [
5512
+ # # 2.5
5513
+ # # 3.5
5514
+ # # 4.5
5515
+ # # ]
5516
+ def mean_horizontal(ignore_nulls: true)
5517
+ select(
5518
+ mean: F.mean_horizontal(F.all, ignore_nulls: ignore_nulls)
5519
+ ).to_series
5520
+ end
5521
+
5522
+ # Aggregate the columns of this DataFrame to their standard deviation value.
5523
+ #
5524
+ # @param ddof [Integer]
5525
+ # Degrees of freedom
5526
+ #
5527
+ # @return [DataFrame]
5528
+ #
5529
+ # @example
5530
+ # df = Polars::DataFrame.new(
5531
+ # {
5532
+ # "foo" => [1, 2, 3],
5533
+ # "bar" => [6, 7, 8],
5534
+ # "ham" => ["a", "b", "c"]
5535
+ # }
5536
+ # )
5537
+ # df.std
5538
+ # # =>
5539
+ # # shape: (1, 3)
5540
+ # # ┌─────┬─────┬──────┐
5541
+ # # │ foo ┆ bar ┆ ham │
5542
+ # # │ --- ┆ --- ┆ --- │
5543
+ # # │ f64 ┆ f64 ┆ str │
5544
+ # # ╞═════╪═════╪══════╡
4094
5545
  # # │ 1.0 ┆ 1.0 ┆ null │
4095
5546
  # # └─────┴─────┴──────┘
4096
5547
  #
@@ -4106,7 +5557,7 @@ module Polars
4106
5557
  # # │ 0.816497 ┆ 0.816497 ┆ null │
4107
5558
  # # └──────────┴──────────┴──────┘
4108
5559
  def std(ddof: 1)
4109
- lazy.std(ddof: ddof).collect(_eager: true)
5560
+ lazy.std(ddof: ddof).collect(optimizations: QueryOptFlags._eager)
4110
5561
  end
4111
5562
 
4112
5563
  # Aggregate the columns of this DataFrame to their variance value.
@@ -4147,7 +5598,7 @@ module Polars
4147
5598
  # # │ 0.666667 ┆ 0.666667 ┆ null │
4148
5599
  # # └──────────┴──────────┴──────┘
4149
5600
  def var(ddof: 1)
4150
- lazy.var(ddof: ddof).collect(_eager: true)
5601
+ lazy.var(ddof: ddof).collect(optimizations: QueryOptFlags._eager)
4151
5602
  end
4152
5603
 
4153
5604
  # Aggregate the columns of this DataFrame to their median value.
@@ -4173,7 +5624,7 @@ module Polars
4173
5624
  # # │ 2.0 ┆ 7.0 ┆ null │
4174
5625
  # # └─────┴─────┴──────┘
4175
5626
  def median
4176
- lazy.median.collect(_eager: true)
5627
+ lazy.median.collect(optimizations: QueryOptFlags._eager)
4177
5628
  end
4178
5629
 
4179
5630
  # Aggregate the columns of this DataFrame to their product values.
@@ -4230,14 +5681,20 @@ module Polars
4230
5681
  # # │ 2.0 ┆ 7.0 ┆ null │
4231
5682
  # # └─────┴─────┴──────┘
4232
5683
  def quantile(quantile, interpolation: "nearest")
4233
- lazy.quantile(quantile, interpolation: interpolation).collect(_eager: true)
5684
+ lazy.quantile(quantile, interpolation: interpolation).collect(optimizations: QueryOptFlags._eager)
4234
5685
  end
4235
5686
 
4236
5687
  # Get one hot encoded dummy variables.
4237
5688
  #
4238
- # @param columns
5689
+ # @param columns [Array]
4239
5690
  # A subset of columns to convert to dummy variables. `nil` means
4240
5691
  # "all columns".
5692
+ # @param separator [String]
5693
+ # Separator/delimiter used when generating column names.
5694
+ # @param drop_first [Boolean]
5695
+ # Remove the first category from the variables being encoded.
5696
+ # @param drop_nulls [Boolean]
5697
+ # If there are `nil` values in the series, a `null` column is not generated
4241
5698
  #
4242
5699
  # @return [DataFrame]
4243
5700
  #
@@ -4260,11 +5717,11 @@ module Polars
4260
5717
  # # │ 1 ┆ 0 ┆ 1 ┆ 0 ┆ 1 ┆ 0 │
4261
5718
  # # │ 0 ┆ 1 ┆ 0 ┆ 1 ┆ 0 ┆ 1 │
4262
5719
  # # └───────┴───────┴───────┴───────┴───────┴───────┘
4263
- def to_dummies(columns: nil, separator: "_", drop_first: false)
5720
+ def to_dummies(columns: nil, separator: "_", drop_first: false, drop_nulls: false)
4264
5721
  if columns.is_a?(::String)
4265
5722
  columns = [columns]
4266
5723
  end
4267
- _from_rbdf(_df.to_dummies(columns, separator, drop_first))
5724
+ _from_rbdf(_df.to_dummies(columns, separator, drop_first, drop_nulls))
4268
5725
  end
4269
5726
 
4270
5727
  # Drop duplicate rows from this DataFrame.
@@ -4291,7 +5748,7 @@ module Polars
4291
5748
  # "c" => [true, true, true, false, true, true]
4292
5749
  # }
4293
5750
  # )
4294
- # df.unique
5751
+ # df.unique(maintain_order: true)
4295
5752
  # # =>
4296
5753
  # # shape: (5, 3)
4297
5754
  # # ┌─────┬─────┬───────┐
@@ -4305,11 +5762,11 @@ module Polars
4305
5762
  # # │ 4 ┆ 3.0 ┆ true │
4306
5763
  # # │ 5 ┆ 3.0 ┆ true │
4307
5764
  # # └─────┴─────┴───────┘
4308
- def unique(maintain_order: true, subset: nil, keep: "first")
5765
+ def unique(maintain_order: false, subset: nil, keep: "any")
4309
5766
  self._from_rbdf(
4310
5767
  lazy
4311
5768
  .unique(maintain_order: maintain_order, subset: subset, keep: keep)
4312
- .collect(no_optimization: true)
5769
+ .collect(optimizations: QueryOptFlags._eager)
4313
5770
  ._df
4314
5771
  )
4315
5772
  end
@@ -4402,9 +5859,9 @@ module Polars
4402
5859
  # Sample from this DataFrame.
4403
5860
  #
4404
5861
  # @param n [Integer]
4405
- # Number of items to return. Cannot be used with `frac`. Defaults to 1 if
4406
- # `frac` is nil.
4407
- # @param frac [Float]
5862
+ # Number of items to return. Cannot be used with `fraction`. Defaults to 1 if
5863
+ # `fraction` is nil.
5864
+ # @param fraction [Float]
4408
5865
  # Fraction of items to return. Cannot be used with `n`.
4409
5866
  # @param with_replacement [Boolean]
4410
5867
  # Allow values to be sampled more than once.
@@ -4432,25 +5889,25 @@ module Polars
4432
5889
  # # │ --- ┆ --- ┆ --- │
4433
5890
  # # │ i64 ┆ i64 ┆ str │
4434
5891
  # # ╞═════╪═════╪═════╡
4435
- # # │ 38c
5892
+ # # │ 16a
4436
5893
  # # │ 2 ┆ 7 ┆ b │
4437
5894
  # # └─────┴─────┴─────┘
4438
5895
  def sample(
4439
5896
  n: nil,
4440
- frac: nil,
5897
+ fraction: nil,
4441
5898
  with_replacement: false,
4442
5899
  shuffle: false,
4443
5900
  seed: nil
4444
5901
  )
4445
- if !n.nil? && !frac.nil?
4446
- raise ArgumentError, "cannot specify both `n` and `frac`"
5902
+ if !n.nil? && !fraction.nil?
5903
+ raise ArgumentError, "cannot specify both `n` and `fraction`"
4447
5904
  end
4448
5905
 
4449
- if n.nil? && !frac.nil?
4450
- frac = Series.new("frac", [frac]) unless frac.is_a?(Series)
5906
+ if n.nil? && !fraction.nil?
5907
+ fraction = Series.new("fraction", [fraction]) unless fraction.is_a?(Series)
4451
5908
 
4452
5909
  return _from_rbdf(
4453
- _df.sample_frac(frac._s, with_replacement, shuffle, seed)
5910
+ _df.sample_frac(fraction._s, with_replacement, shuffle, seed)
4454
5911
  )
4455
5912
  end
4456
5913
 
@@ -4658,6 +6115,85 @@ module Polars
4658
6115
  end
4659
6116
  end
4660
6117
 
6118
+ # Convert columnar data to rows as Ruby arrays in a hash keyed by some column.
6119
+ #
6120
+ # This method is like `rows`, but instead of returning rows in a flat list, rows
6121
+ # are grouped by the values in the `key` column(s) and returned as a hash.
6122
+ #
6123
+ # Note that this method should not be used in place of native operations, due to
6124
+ # the high cost of materializing all frame data out into a hash; it should
6125
+ # be used only when you need to move the values out into a Ruby data structure
6126
+ # or other object that cannot operate directly with Polars/Arrow.
6127
+ #
6128
+ # @param key [Object]
6129
+ # The column(s) to use as the key for the returned hash. If multiple
6130
+ # columns are specified, the key will be a tuple of those values, otherwise
6131
+ # it will be a string.
6132
+ # @param named [Boolean]
6133
+ # Return hashes instead of arrays. The hashes are a mapping of
6134
+ # column name to row value. This is more expensive than returning an
6135
+ # array, but allows for accessing values by column name.
6136
+ # @param include_key [Boolean]
6137
+ # Include key values inline with the associated data (by default the key
6138
+ # values are omitted as a memory/performance optimisation, as they can be
6139
+ # reoconstructed from the key).
6140
+ # @param unique [Boolean]
6141
+ # Indicate that the key is unique; this will result in a 1:1 mapping from
6142
+ # key to a single associated row. Note that if the key is *not* actually
6143
+ # unique the last row with the given key will be returned.
6144
+ #
6145
+ # @return [Hash]
6146
+ #
6147
+ # @example Group rows by the given key column(s):
6148
+ # df = Polars::DataFrame.new(
6149
+ # {
6150
+ # "w" => ["a", "b", "b", "a"],
6151
+ # "x" => ["q", "q", "q", "k"],
6152
+ # "y" => [1.0, 2.5, 3.0, 4.5],
6153
+ # "z" => [9, 8, 7, 6]
6154
+ # }
6155
+ # )
6156
+ # df.rows_by_key(["w"])
6157
+ # # => {"a"=>[["q", 1.0, 9], ["k", 4.5, 6]], "b"=>[["q", 2.5, 8], ["q", 3.0, 7]]}
6158
+ #
6159
+ # @example Return the same row groupings as hashes:
6160
+ # df.rows_by_key(["w"], named: true)
6161
+ # # => {"a"=>[{"x"=>"q", "y"=>1.0, "z"=>9}, {"x"=>"k", "y"=>4.5, "z"=>6}], "b"=>[{"x"=>"q", "y"=>2.5, "z"=>8}, {"x"=>"q", "y"=>3.0, "z"=>7}]}
6162
+ #
6163
+ # @example Return row groupings, assuming keys are unique:
6164
+ # df.rows_by_key(["z"], unique: true)
6165
+ # # => {9=>["a", "q", 1.0], 8=>["b", "q", 2.5], 7=>["b", "q", 3.0], 6=>["a", "k", 4.5]}
6166
+ #
6167
+ # @example Return row groupings as hashes, assuming keys are unique:
6168
+ # df.rows_by_key(["z"], named: true, unique: true)
6169
+ # # => {9=>{"w"=>"a", "x"=>"q", "y"=>1.0}, 8=>{"w"=>"b", "x"=>"q", "y"=>2.5}, 7=>{"w"=>"b", "x"=>"q", "y"=>3.0}, 6=>{"w"=>"a", "x"=>"k", "y"=>4.5}}
6170
+ #
6171
+ # @example Return hash rows grouped by a compound key, including key values:
6172
+ # df.rows_by_key(["w", "x"], named: true, include_key: true)
6173
+ # # => {["a", "q"]=>[{"w"=>"a", "x"=>"q", "y"=>1.0, "z"=>9}], ["b", "q"]=>[{"w"=>"b", "x"=>"q", "y"=>2.5, "z"=>8}, {"w"=>"b", "x"=>"q", "y"=>3.0, "z"=>7}], ["a", "k"]=>[{"w"=>"a", "x"=>"k", "y"=>4.5, "z"=>6}]}
6174
+ def rows_by_key(key, named: false, include_key: false, unique: false)
6175
+ key = Utils._expand_selectors(self, key)
6176
+
6177
+ keys = key.size == 1 ? get_column(key[0]) : select(key).iter_rows
6178
+
6179
+ if include_key
6180
+ values = self
6181
+ else
6182
+ data_cols = schema.names - key
6183
+ values = select(data_cols)
6184
+ end
6185
+
6186
+ zipped = keys.each.zip(values.iter_rows(named: named))
6187
+
6188
+ # if unique, we expect to write just one entry per key; otherwise, we're
6189
+ # returning a list of rows for each key, so append into a hash of arrays.
6190
+ if unique
6191
+ zipped.to_h
6192
+ else
6193
+ zipped.each_with_object({}) { |(key, data), h| (h[key] ||= []) << data }
6194
+ end
6195
+ end
6196
+
4661
6197
  # Returns an iterator over the DataFrame of rows of Ruby-native values.
4662
6198
  #
4663
6199
  # @param named [Boolean]
@@ -4686,7 +6222,7 @@ module Polars
4686
6222
  # @example
4687
6223
  # df.iter_rows(named: true).map { |row| row["b"] }
4688
6224
  # # => [2, 4, 6]
4689
- def iter_rows(named: false, buffer_size: 500, &block)
6225
+ def iter_rows(named: false, buffer_size: 512, &block)
4690
6226
  return to_enum(:iter_rows, named: named, buffer_size: buffer_size) unless block_given?
4691
6227
 
4692
6228
  # load into the local namespace for a modest performance boost in the hot loops
@@ -4737,6 +6273,90 @@ module Polars
4737
6273
  iter_rows(named: named, buffer_size: buffer_size, &block)
4738
6274
  end
4739
6275
 
6276
+ # Returns an iterator over the columns of this DataFrame.
6277
+ #
6278
+ # @return [Object]
6279
+ #
6280
+ # @note
6281
+ # Consider whether you can use `all` instead.
6282
+ # If you can, it will be more efficient.
6283
+ #
6284
+ # @example
6285
+ # df = Polars::DataFrame.new(
6286
+ # {
6287
+ # "a" => [1, 3, 5],
6288
+ # "b" => [2, 4, 6]
6289
+ # }
6290
+ # )
6291
+ # df.iter_columns.map { |s| s.name }
6292
+ # # => ["a", "b"]
6293
+ #
6294
+ # @example If you're using this to modify a dataframe's columns, e.g.
6295
+ # # Do NOT do this
6296
+ # Polars::DataFrame.new(df.iter_columns.map { |column| column * 2 })
6297
+ # # =>
6298
+ # # shape: (3, 2)
6299
+ # # ┌─────┬─────┐
6300
+ # # │ a ┆ b │
6301
+ # # │ --- ┆ --- │
6302
+ # # │ i64 ┆ i64 │
6303
+ # # ╞═════╪═════╡
6304
+ # # │ 2 ┆ 4 │
6305
+ # # │ 6 ┆ 8 │
6306
+ # # │ 10 ┆ 12 │
6307
+ # # └─────┴─────┘
6308
+ #
6309
+ # @example then consider whether you can use `all` instead:
6310
+ # df.select(Polars.all * 2)
6311
+ # # =>
6312
+ # # shape: (3, 2)
6313
+ # # ┌─────┬─────┐
6314
+ # # │ a ┆ b │
6315
+ # # │ --- ┆ --- │
6316
+ # # │ i64 ┆ i64 │
6317
+ # # ╞═════╪═════╡
6318
+ # # │ 2 ┆ 4 │
6319
+ # # │ 6 ┆ 8 │
6320
+ # # │ 10 ┆ 12 │
6321
+ # # └─────┴─────┘
6322
+ def iter_columns
6323
+ return to_enum(:iter_columns) unless block_given?
6324
+
6325
+ _df.get_columns.each do |s|
6326
+ yield Utils.wrap_s(s)
6327
+ end
6328
+ end
6329
+
6330
+ # Returns a non-copying iterator of slices over the underlying DataFrame.
6331
+ #
6332
+ # @param n_rows [Integer]
6333
+ # Determines the number of rows contained in each DataFrame slice.
6334
+ #
6335
+ # @return [Object]
6336
+ #
6337
+ # @example
6338
+ # df = Polars::DataFrame.new(
6339
+ # {
6340
+ # "a" => 0...17_500,
6341
+ # "b" => Date.new(2023, 1, 1),
6342
+ # "c" => "klmnoopqrstuvwxyz"
6343
+ # },
6344
+ # schema_overrides: {"a" => Polars::Int32}
6345
+ # )
6346
+ # df.iter_slices.map.with_index do |frame, idx|
6347
+ # "#{frame.class.name}:[#{idx}]:#{frame.length}"
6348
+ # end
6349
+ # # => ["Polars::DataFrame:[0]:10000", "Polars::DataFrame:[1]:7500"]
6350
+ def iter_slices(n_rows: 10_000)
6351
+ return to_enum(:iter_slices, n_rows: n_rows) unless block_given?
6352
+
6353
+ offset = 0
6354
+ while offset < height
6355
+ yield slice(offset, n_rows)
6356
+ offset += n_rows
6357
+ end
6358
+ end
6359
+
4740
6360
  # Shrink DataFrame memory usage.
4741
6361
  #
4742
6362
  # Shrinks to fit the exact capacity needed to hold the data.
@@ -4773,11 +6393,10 @@ module Polars
4773
6393
  def gather_every(n, offset = 0)
4774
6394
  select(F.col("*").gather_every(n, offset))
4775
6395
  end
4776
- alias_method :take_every, :gather_every
4777
6396
 
4778
6397
  # Hash and combine the rows in this DataFrame.
4779
6398
  #
4780
- # The hash value is of type `:u64`.
6399
+ # The hash value is of type `UInt64`.
4781
6400
  #
4782
6401
  # @param seed [Integer]
4783
6402
  # Random seed parameter. Defaults to 0.
@@ -4884,8 +6503,8 @@ module Polars
4884
6503
  # # {4,"four"}
4885
6504
  # # {5,"five"}
4886
6505
  # # ]
4887
- def to_struct(name)
4888
- Utils.wrap_s(_df.to_struct(name))
6506
+ def to_struct(name = "")
6507
+ Utils.wrap_s(_df.to_struct(name, []))
4889
6508
  end
4890
6509
 
4891
6510
  # Decompose a struct into its fields.
@@ -4893,8 +6512,13 @@ module Polars
4893
6512
  # The fields will be inserted into the `DataFrame` on the location of the
4894
6513
  # `struct` type.
4895
6514
  #
4896
- # @param names [Object]
4897
- # Names of the struct columns that will be decomposed by its fields
6515
+ # @param columns [Object]
6516
+ # Name of the struct column(s) that should be unnested.
6517
+ # @param more_columns [Array]
6518
+ # Additional columns to unnest, specified as positional arguments.
6519
+ # @param separator [String]
6520
+ # Rename output column names as combination of the struct column name,
6521
+ # name separator and field name.
4898
6522
  #
4899
6523
  # @return [DataFrame]
4900
6524
  #
@@ -4920,17 +6544,10 @@ module Polars
4920
6544
  # # │ foo ┆ 1 ┆ a ┆ true ┆ [1, 2] ┆ baz │
4921
6545
  # # │ bar ┆ 2 ┆ b ┆ null ┆ [3] ┆ womp │
4922
6546
  # # └────────┴─────┴─────┴──────┴───────────┴───────┘
4923
- def unnest(names)
4924
- if names.is_a?(::String)
4925
- names = [names]
4926
- end
4927
- _from_rbdf(_df.unnest(names))
6547
+ def unnest(columns = nil, *more_columns, separator: nil)
6548
+ lazy.unnest(columns, *more_columns, separator: separator).collect(optimizations: QueryOptFlags._eager)
4928
6549
  end
4929
6550
 
4930
- # Requires NumPy
4931
- # def corr
4932
- # end
4933
-
4934
6551
  # Take two sorted DataFrames and merge them by the sorted key.
4935
6552
  #
4936
6553
  # The output of this operation will also be sorted.
@@ -4943,6 +6560,10 @@ module Polars
4943
6560
  # Other DataFrame that must be merged
4944
6561
  # @param key [String]
4945
6562
  # Key that is sorted.
6563
+ # @param maintain_order [Boolean]
6564
+ # If `true`, the output is guaranteed to have left-biased ordering
6565
+ # for equal keys: rows from the left frame appear before rows from
6566
+ # the right frame when their keys are equal.
4946
6567
  #
4947
6568
  # @return [DataFrame]
4948
6569
  #
@@ -4969,30 +6590,235 @@ module Polars
4969
6590
  # # │ steve ┆ 42 │
4970
6591
  # # │ elise ┆ 44 │
4971
6592
  # # └────────┴─────┘
4972
- def merge_sorted(other, key)
4973
- lazy.merge_sorted(other.lazy, key).collect(_eager: true)
6593
+ def merge_sorted(other, key, maintain_order: false)
6594
+ lazy.merge_sorted(other.lazy, key, maintain_order: maintain_order).collect(optimizations: QueryOptFlags._eager)
4974
6595
  end
4975
6596
 
4976
- # Indicate that one or multiple columns are sorted.
6597
+ # Flag a column as sorted.
6598
+ #
6599
+ # This can speed up future operations.
6600
+ #
6601
+ # @note
6602
+ # This can lead to incorrect results if the data is NOT sorted! Use with care!
4977
6603
  #
4978
6604
  # @param column [Object]
4979
- # Columns that are sorted
6605
+ # Column that is sorted.
4980
6606
  # @param descending [Boolean]
4981
- # Whether the columns are sorted in descending order.
6607
+ # Whether the column is sorted in descending order.
6608
+ # @param nulls_last [Boolean]
6609
+ # Whether the nulls are at the end.
4982
6610
  #
4983
6611
  # @return [DataFrame]
4984
6612
  def set_sorted(
4985
6613
  column,
4986
- descending: false
6614
+ descending: false,
6615
+ nulls_last: false
4987
6616
  )
4988
6617
  lazy
4989
- .set_sorted(column, descending: descending)
4990
- .collect(no_optimization: true)
6618
+ .set_sorted(column, descending: descending, nulls_last: nulls_last)
6619
+ .collect(optimizations: QueryOptFlags._eager)
4991
6620
  end
4992
6621
 
4993
- # TODO
4994
- # def update
4995
- # end
6622
+ # Update the values in this `DataFrame` with the values in `other`.
6623
+ #
6624
+ # @note
6625
+ # This functionality is considered **unstable**. It may be changed
6626
+ # at any point without it being considered a breaking change.
6627
+ #
6628
+ # @param other [DataFrame]
6629
+ # DataFrame that will be used to update the values
6630
+ # @param on [Object]
6631
+ # Column names that will be joined on. If set to `nil` (default),
6632
+ # the implicit row index of each frame is used as a join key.
6633
+ # @param how ['left', 'inner', 'full']
6634
+ # * 'left' will keep all rows from the left table; rows may be duplicated
6635
+ # if multiple rows in the right frame match the left row's key.
6636
+ # * 'inner' keeps only those rows where the key exists in both frames.
6637
+ # * 'full' will update existing rows where the key matches while also
6638
+ # adding any new rows contained in the given frame.
6639
+ # @param left_on [Object]
6640
+ # Join column(s) of the left DataFrame.
6641
+ # @param right_on [Object]
6642
+ # Join column(s) of the right DataFrame.
6643
+ # @param include_nulls [Boolean]
6644
+ # Overwrite values in the left frame with null values from the right frame.
6645
+ # If set to `false` (default), null values in the right frame are ignored.
6646
+ # @param maintain_order ['none', 'left', 'right', 'left_right', 'right_left']
6647
+ # Which order of rows from the inputs to preserve. See `DataFrame.join`
6648
+ # for details. Unlike `join` this function preserves the left order by
6649
+ # default.
6650
+ #
6651
+ # @return [DataFrame]
6652
+ #
6653
+ # @note
6654
+ # This is syntactic sugar for a left/inner join that preserves the order
6655
+ # of the left `DataFrame` by default, with an optional coalesce when
6656
+ # `include_nulls: false`.
6657
+ #
6658
+ # @example Update `df` values with the non-null values in `new_df`, by row index:
6659
+ # df = Polars::DataFrame.new(
6660
+ # {
6661
+ # "A" => [1, 2, 3, 4],
6662
+ # "B" => [400, 500, 600, 700]
6663
+ # }
6664
+ # )
6665
+ # new_df = Polars::DataFrame.new(
6666
+ # {
6667
+ # "B" => [-66, nil, -99],
6668
+ # "C" => [5, 3, 1]
6669
+ # }
6670
+ # )
6671
+ # df.update(new_df)
6672
+ # # =>
6673
+ # # shape: (4, 2)
6674
+ # # ┌─────┬─────┐
6675
+ # # │ A ┆ B │
6676
+ # # │ --- ┆ --- │
6677
+ # # │ i64 ┆ i64 │
6678
+ # # ╞═════╪═════╡
6679
+ # # │ 1 ┆ -66 │
6680
+ # # │ 2 ┆ 500 │
6681
+ # # │ 3 ┆ -99 │
6682
+ # # │ 4 ┆ 700 │
6683
+ # # └─────┴─────┘
6684
+ #
6685
+ # @example Update `df` values with the non-null values in `new_df`, by row index, but only keeping those rows that are common to both frames:
6686
+ # df.update(new_df, how: "inner")
6687
+ # # =>
6688
+ # # shape: (3, 2)
6689
+ # # ┌─────┬─────┐
6690
+ # # │ A ┆ B │
6691
+ # # │ --- ┆ --- │
6692
+ # # │ i64 ┆ i64 │
6693
+ # # ╞═════╪═════╡
6694
+ # # │ 1 ┆ -66 │
6695
+ # # │ 2 ┆ 500 │
6696
+ # # │ 3 ┆ -99 │
6697
+ # # └─────┴─────┘
6698
+ #
6699
+ # @example Update `df` values with the non-null values in `new_df`, using a full outer join strategy that defines explicit join columns in each frame:
6700
+ # df.update(new_df, left_on: ["A"], right_on: ["C"], how: "full")
6701
+ # # =>
6702
+ # # shape: (5, 2)
6703
+ # # ┌─────┬─────┐
6704
+ # # │ A ┆ B │
6705
+ # # │ --- ┆ --- │
6706
+ # # │ i64 ┆ i64 │
6707
+ # # ╞═════╪═════╡
6708
+ # # │ 1 ┆ -99 │
6709
+ # # │ 2 ┆ 500 │
6710
+ # # │ 3 ┆ 600 │
6711
+ # # │ 4 ┆ 700 │
6712
+ # # │ 5 ┆ -66 │
6713
+ # # └─────┴─────┘
6714
+ #
6715
+ # @example Update `df` values including null values in `new_df`, using a full outer join strategy that defines explicit join columns in each frame:
6716
+ # df.update(new_df, left_on: "A", right_on: "C", how: "full", include_nulls: true)
6717
+ # # =>
6718
+ # # shape: (5, 2)
6719
+ # # ┌─────┬──────┐
6720
+ # # │ A ┆ B │
6721
+ # # │ --- ┆ --- │
6722
+ # # │ i64 ┆ i64 │
6723
+ # # ╞═════╪══════╡
6724
+ # # │ 1 ┆ -99 │
6725
+ # # │ 2 ┆ 500 │
6726
+ # # │ 3 ┆ null │
6727
+ # # │ 4 ┆ 700 │
6728
+ # # │ 5 ┆ -66 │
6729
+ # # └─────┴──────┘
6730
+ def update(
6731
+ other,
6732
+ on: nil,
6733
+ how: "left",
6734
+ left_on: nil,
6735
+ right_on: nil,
6736
+ include_nulls: false,
6737
+ maintain_order: "left"
6738
+ )
6739
+ Utils.require_same_type(self, other)
6740
+ lazy
6741
+ .update(
6742
+ other.lazy,
6743
+ on: on,
6744
+ how: how,
6745
+ left_on: left_on,
6746
+ right_on: right_on,
6747
+ include_nulls: include_nulls,
6748
+ maintain_order: maintain_order
6749
+ )
6750
+ .collect(optimizations: QueryOptFlags._eager)
6751
+ end
6752
+
6753
+ # Match or evolve the schema of a LazyFrame into a specific schema.
6754
+ #
6755
+ # By default, match_to_schema returns an error if the input schema does not
6756
+ # exactly match the target schema. It also allows columns to be freely reordered,
6757
+ # with additional coercion rules available through optional parameters.
6758
+ #
6759
+ # @note
6760
+ # This functionality is considered **unstable**. It may be changed
6761
+ # at any point without it being considered a breaking change.
6762
+ #
6763
+ # @param schema [Object]
6764
+ # Target schema to match or evolve to.
6765
+ # @param missing_columns [Object]
6766
+ # Raise of insert missing columns from the input with respect to the `schema`.
6767
+ #
6768
+ # This can also be an expression per column with what to insert if it is
6769
+ # missing.
6770
+ # @param missing_struct_fields [Object]
6771
+ # Raise of insert missing struct fields from the input with respect to the
6772
+ # `schema`.
6773
+ # @param extra_columns [Object]
6774
+ # Raise of ignore extra columns from the input with respect to the `schema`.
6775
+ # @param extra_struct_fields [Object]
6776
+ # Raise of ignore extra struct fields from the input with respect to the
6777
+ # `schema`.
6778
+ # @param integer_cast [Object]
6779
+ # Forbid of upcast for integer columns from the input to the respective column
6780
+ # in `schema`.
6781
+ # @param float_cast [Object]
6782
+ # Forbid of upcast for float columns from the input to the respective column
6783
+ # in `schema`.
6784
+ #
6785
+ # @return [DataFrame]
6786
+ #
6787
+ # @example Ensuring the schema matches
6788
+ # df = Polars::DataFrame.new({"a" => [1, 2, 3], "b" => ["A", "B", "C"]})
6789
+ # df.match_to_schema({"a" => Polars::Int64, "b" => Polars::String})
6790
+ # # =>
6791
+ # # shape: (3, 2)
6792
+ # # ┌─────┬─────┐
6793
+ # # │ a ┆ b │
6794
+ # # │ --- ┆ --- │
6795
+ # # │ i64 ┆ str │
6796
+ # # ╞═════╪═════╡
6797
+ # # │ 1 ┆ A │
6798
+ # # │ 2 ┆ B │
6799
+ # # │ 3 ┆ C │
6800
+ # # └─────┴─────┘
6801
+ def match_to_schema(
6802
+ schema,
6803
+ missing_columns: "raise",
6804
+ missing_struct_fields: "raise",
6805
+ extra_columns: "raise",
6806
+ extra_struct_fields: "raise",
6807
+ integer_cast: "forbid",
6808
+ float_cast: "forbid"
6809
+ )
6810
+ lazy
6811
+ .match_to_schema(
6812
+ schema,
6813
+ missing_columns: missing_columns,
6814
+ missing_struct_fields: missing_struct_fields,
6815
+ extra_columns: extra_columns,
6816
+ extra_struct_fields: extra_struct_fields,
6817
+ integer_cast: integer_cast,
6818
+ float_cast: float_cast
6819
+ )
6820
+ .collect(optimizations: QueryOptFlags._eager)
6821
+ end
4996
6822
 
4997
6823
  private
4998
6824
 
@@ -5057,282 +6883,6 @@ module Polars
5057
6883
  raise ArgumentError, "Unsupported idxs datatype."
5058
6884
  end
5059
6885
 
5060
- # @private
5061
- def self.expand_hash_scalars(data, schema_overrides: nil, strict: true, order: nil, nan_to_null: false)
5062
- updated_data = {}
5063
- unless data.empty?
5064
- dtypes = schema_overrides || {}
5065
- array_len = data.values.map { |val| Utils.arrlen(val) || 0 }.max
5066
- if array_len > 0
5067
- data.each do |name, val|
5068
- dtype = dtypes[name]
5069
- if val.is_a?(Hash) && dtype != Struct
5070
- updated_data[name] = DataFrame.new(val, strict: strict).to_struct(name)
5071
- elsif !Utils.arrlen(val).nil?
5072
- updated_data[name] = Series.new(::String.new(name), val, dtype: dtype, strict: strict)
5073
- elsif val.nil? || [Integer, Float, TrueClass, FalseClass, ::String, ::Date, ::DateTime, ::Time].any? { |cls| val.is_a?(cls) }
5074
- dtype = Polars::Float64 if val.nil? && dtype.nil?
5075
- updated_data[name] = Series.new(::String.new(name), [val], dtype: dtype, strict: strict).extend_constant(val, array_len - 1)
5076
- else
5077
- raise Todo
5078
- end
5079
- end
5080
- elsif data.values.all? { |val| Utils.arrlen(val) == 0 }
5081
- data.each do |name, val|
5082
- updated_data[name] = Series.new(name, val, dtype: dtypes[name], strict: strict)
5083
- end
5084
- elsif data.values.all? { |val| Utils.arrlen(val).nil? }
5085
- data.each do |name, val|
5086
- updated_data[name] = Series.new(name, [val], dtype: dtypes[name], strict: strict)
5087
- end
5088
- end
5089
- end
5090
- updated_data
5091
- end
5092
-
5093
- # @private
5094
- def self.hash_to_rbdf(data, schema: nil, schema_overrides: nil, strict: true, nan_to_null: nil)
5095
- if schema.is_a?(Hash) && !data.empty?
5096
- if !data.all? { |col, _| schema[col] }
5097
- raise ArgumentError, "The given column-schema names do not match the data dictionary"
5098
- end
5099
-
5100
- data = schema.to_h { |col| [col, data[col]] }
5101
- end
5102
-
5103
- column_names, schema_overrides = _unpack_schema(
5104
- schema, lookup_names: data.keys, schema_overrides: schema_overrides
5105
- )
5106
- if column_names.empty?
5107
- column_names = data.keys
5108
- end
5109
-
5110
- if data.empty? && !schema_overrides.empty?
5111
- data_series = column_names.map { |name| Series.new(name, [], dtype: schema_overrides[name], strict: strict, nan_to_null: nan_to_null)._s }
5112
- else
5113
- data_series = expand_hash_scalars(data, schema_overrides: schema_overrides, strict: strict, nan_to_null: nan_to_null).values.map(&:_s)
5114
- end
5115
-
5116
- data_series = _handle_columns_arg(data_series, columns: column_names, from_hash: true)
5117
- RbDataFrame.new(data_series)
5118
- end
5119
-
5120
- # @private
5121
- def self.include_unknowns(schema, cols)
5122
- cols.to_h { |col| [col, schema.fetch(col, Unknown)] }
5123
- end
5124
-
5125
- # @private
5126
- def self._unpack_schema(schema, schema_overrides: nil, n_expected: nil, lookup_names: nil, include_overrides_in_columns: false)
5127
- if schema.is_a?(Hash)
5128
- schema = schema.to_a
5129
- end
5130
- column_names =
5131
- (schema || []).map.with_index do |col, i|
5132
- if col.is_a?(::String)
5133
- col || "column_#{i}"
5134
- else
5135
- col[0]
5136
- end
5137
- end
5138
- if column_names.empty? && n_expected
5139
- column_names = n_expected.times.map { |i| "column_#{i}" }
5140
- end
5141
- # TODO zip_longest
5142
- lookup = column_names.zip(lookup_names || []).to_h
5143
-
5144
- column_dtypes =
5145
- (schema || []).select { |col| !col.is_a?(::String) && col[1] }.to_h do |col|
5146
- [lookup[col[0]] || col[0], col[1]]
5147
- end
5148
-
5149
- if schema_overrides && schema_overrides.any?
5150
- column_dtypes.merge!(schema_overrides)
5151
- end
5152
-
5153
- column_dtypes.each do |col, dtype|
5154
- if !Utils.is_polars_dtype(dtype, include_unknown: true) && !dtype.nil?
5155
- column_dtypes[col] = Utils.rb_type_to_dtype(dtype)
5156
- end
5157
- end
5158
-
5159
- [column_names, column_dtypes]
5160
- end
5161
-
5162
- def self._handle_columns_arg(data, columns: nil, from_hash: false)
5163
- if columns.nil? || columns.empty?
5164
- data
5165
- else
5166
- if data.empty?
5167
- columns.map { |c| Series.new(c, nil)._s }
5168
- elsif data.length == columns.length
5169
- if from_hash
5170
- series_map = data.to_h { |s| [s.name, s] }
5171
- if columns.all? { |col| series_map.key?(col) }
5172
- return columns.map { |col| series_map[col] }
5173
- end
5174
- end
5175
-
5176
- columns.each_with_index do |c, i|
5177
- # not in-place?
5178
- data[i].rename(c)
5179
- end
5180
- data
5181
- else
5182
- raise ArgumentError, "Dimensions of columns arg must match data dimensions."
5183
- end
5184
- end
5185
- end
5186
-
5187
- def self._post_apply_columns(rbdf, columns, structs: nil, schema_overrides: nil, strict: true)
5188
- rbdf_columns = rbdf.columns
5189
- rbdf_dtypes = rbdf.dtypes
5190
- columns, dtypes = _unpack_schema(
5191
- (columns || rbdf_columns), schema_overrides: schema_overrides
5192
- )
5193
- column_subset = []
5194
- if columns != rbdf_columns
5195
- if columns.length < rbdf_columns.length && columns == rbdf_columns.first(columns.length)
5196
- column_subset = columns
5197
- else
5198
- rbdf.set_column_names(columns)
5199
- end
5200
- end
5201
-
5202
- column_casts = []
5203
- columns.each_with_index do |col, i|
5204
- if dtypes[col] == Categorical # != rbdf_dtypes[i]
5205
- column_casts << Polars.col(col).cast(Categorical, strict: strict)._rbexpr
5206
- elsif structs&.any? && structs.include?(col) && structs[col] != rbdf_dtypes[i]
5207
- column_casts << Polars.col(col).cast(structs[col], strict: strict)._rbexpr
5208
- elsif dtypes.include?(col) && dtypes[col] != rbdf_dtypes[i]
5209
- column_casts << Polars.col(col).cast(dtypes[col], strict: strict)._rbexpr
5210
- end
5211
- end
5212
-
5213
- if column_casts.any? || column_subset.any?
5214
- rbdf = rbdf.lazy
5215
- if column_casts.any?
5216
- rbdf = rbdf.with_columns(column_casts)
5217
- end
5218
- if column_subset.any?
5219
- rbdf = rbdf.select(column_subset.map { |col| Polars.col(col)._rbexpr })
5220
- end
5221
- rbdf = rbdf.collect
5222
- end
5223
-
5224
- rbdf
5225
- end
5226
-
5227
- # @private
5228
- def self.sequence_to_rbdf(data, schema: nil, schema_overrides: nil, strict: true, orient: nil, infer_schema_length: 50)
5229
- columns = schema
5230
-
5231
- if data.length == 0
5232
- return hash_to_rbdf({}, schema: schema, schema_overrides: schema_overrides, strict: strict)
5233
- end
5234
-
5235
- if data[0].is_a?(Series)
5236
- # series_names = data.map(&:name)
5237
- # columns, dtypes = _unpack_schema(columns || series_names, n_expected: data.length)
5238
- data_series = []
5239
- data.each do |s|
5240
- data_series << s._s
5241
- end
5242
- elsif data[0].is_a?(Hash)
5243
- column_names, dtypes = _unpack_schema(columns)
5244
- schema_overrides = dtypes ? include_unknowns(dtypes, column_names) : nil
5245
- rbdf = RbDataFrame.from_hashes(data, schema, schema_overrides, strict, infer_schema_length)
5246
- if column_names
5247
- rbdf = _post_apply_columns(rbdf, column_names)
5248
- end
5249
- return rbdf
5250
- elsif data[0].is_a?(::Array)
5251
- first_element = data[0]
5252
- if orient.nil? && !columns.nil?
5253
- row_types = first_element.filter_map { |value| value.class }.uniq
5254
- if row_types.include?(Integer) && row_types.include?(Float)
5255
- row_types.delete(Integer)
5256
- end
5257
- orient = row_types.length == 1 ? "col" : "row"
5258
- end
5259
-
5260
- if orient == "row"
5261
- column_names, schema_overrides = _unpack_schema(
5262
- schema, schema_overrides: schema_overrides, n_expected: first_element.length
5263
- )
5264
- local_schema_override = (
5265
- schema_overrides.any? ? _include_unknowns(schema_overrides, column_names) : {}
5266
- )
5267
- if column_names.any? && first_element.length > 0 && first_element.length != column_names.length
5268
- raise ArgumentError, "the row data does not match the number of columns"
5269
- end
5270
-
5271
- unpack_nested = false
5272
- local_schema_override.each do |col, tp|
5273
- if [Categorical, Enum].include?(tp)
5274
- local_schema_override[col] = String
5275
- elsif !unpack_nested && [Unknown, Struct].include?(tp.base_type)
5276
- raise Todo
5277
- end
5278
- end
5279
-
5280
- if unpack_nested
5281
- raise Todo
5282
- else
5283
- rbdf = RbDataFrame.from_rows(
5284
- data,
5285
- infer_schema_length,
5286
- local_schema_override.any? ? local_schema_override : nil
5287
- )
5288
- end
5289
- if column_names.any? || schema_overrides.any?
5290
- rbdf = _post_apply_columns(
5291
- rbdf, column_names, schema_overrides: schema_overrides, strict: strict
5292
- )
5293
- end
5294
- return rbdf
5295
- elsif orient == "col" || orient.nil?
5296
- column_names, schema_overrides = _unpack_schema(
5297
- schema, schema_overrides: schema_overrides, n_expected: data.length
5298
- )
5299
- data_series =
5300
- data.map.with_index do |element, i|
5301
- Series.new(column_names[i], element, dtype: schema_overrides[column_names[i]], strict: strict)._s
5302
- end
5303
- return RbDataFrame.new(data_series)
5304
- else
5305
- raise ArgumentError, "orient must be one of {{'col', 'row', nil}}, got #{orient} instead."
5306
- end
5307
- end
5308
-
5309
- data_series = _handle_columns_arg(data_series, columns: columns)
5310
- RbDataFrame.new(data_series)
5311
- end
5312
-
5313
- # @private
5314
- def self._include_unknowns(schema, cols)
5315
- cols.to_h { |col| [col, schema[col] || Unknown] }
5316
- end
5317
-
5318
- # @private
5319
- def self.series_to_rbdf(data, schema: nil, schema_overrides: nil, strict: true)
5320
- data_series = [data._s]
5321
- series_name = data_series.map(&:name)
5322
- column_names, schema_overrides = _unpack_schema(
5323
- schema || series_name, schema_overrides: schema_overrides, n_expected: 1
5324
- )
5325
- if schema_overrides.any?
5326
- new_dtype = schema_overrides.values[0]
5327
- if new_dtype != data.dtype
5328
- data_series[0] = data_series[0].cast(new_dtype, strict)
5329
- end
5330
- end
5331
-
5332
- data_series = _handle_columns_arg(data_series, columns: column_names)
5333
- RbDataFrame.new(data_series)
5334
- end
5335
-
5336
6886
  def wrap_ldf(ldf)
5337
6887
  LazyFrame._from_rbldf(ldf)
5338
6888
  end
@@ -5341,6 +6891,11 @@ module Polars
5341
6891
  self.class._from_rbdf(rb_df)
5342
6892
  end
5343
6893
 
6894
+ def _replace(column, new_column)
6895
+ self._df.replace(column, new_column._s)
6896
+ self
6897
+ end
6898
+
5344
6899
  def _comp(other, op)
5345
6900
  if other.is_a?(DataFrame)
5346
6901
  _compare_to_other_df(other, op)
@@ -5358,7 +6913,7 @@ module Polars
5358
6913
  end
5359
6914
 
5360
6915
  suffix = "__POLARS_CMP_OTHER"
5361
- other_renamed = other.select(Polars.all.suffix(suffix))
6916
+ other_renamed = other.select(Polars.all.name.suffix(suffix))
5362
6917
  combined = Polars.concat([self, other_renamed], how: "horizontal")
5363
6918
 
5364
6919
  expr = case op
@@ -5410,5 +6965,284 @@ module Polars
5410
6965
  end
5411
6966
  other
5412
6967
  end
6968
+
6969
+ def with_connection(connection, &block)
6970
+ if !connection.nil?
6971
+ yield connection
6972
+ else
6973
+ ActiveRecord::Base.connection_pool.with_connection(&block)
6974
+ end
6975
+ end
6976
+
6977
+ def maybe_transaction(connection, create_table, &block)
6978
+ if create_table && connection.adapter_name.match?(/postg|sqlite/i) && connection.open_transactions == 0
6979
+ connection.transaction(&block)
6980
+ else
6981
+ yield
6982
+ end
6983
+ end
6984
+
6985
+ def get_series_item_by_key(s, key)
6986
+ if key.is_a?(Integer)
6987
+ return s._s.get_index_signed(key)
6988
+
6989
+ elsif key.is_a?(Range)
6990
+ return _select_elements_by_slice(s, key)
6991
+
6992
+ elsif key.is_a?(::Array)
6993
+ if key.empty?
6994
+ return s.clear
6995
+ end
6996
+
6997
+ first = key[0]
6998
+ if Utils.bool?(first)
6999
+ _raise_on_boolean_mask
7000
+ end
7001
+
7002
+ begin
7003
+ indices = Series.new("", key, dtype: Int64)
7004
+ rescue TypeError
7005
+ msg = "cannot select elements using Sequence with elements of type #{first.class.name.inspect}"
7006
+ raise TypeError, msg
7007
+ end
7008
+
7009
+ indices = _convert_series_to_indices(indices, s.len)
7010
+ return _select_elements_by_index(s, indices)
7011
+
7012
+ elsif key.is_a?(Series)
7013
+ indices = _convert_series_to_indices(key, s.len)
7014
+ return _select_elements_by_index(s, indices)
7015
+ end
7016
+
7017
+ msg = "cannot select elements using key of type #{key.class.name.inspect}: #{key.inspect}"
7018
+ raise TypeError, msg
7019
+ end
7020
+
7021
+ def _select_elements_by_slice(s, key)
7022
+ Slice.new(s).apply(key)
7023
+ end
7024
+
7025
+ def _select_elements_by_index(s, key)
7026
+ s.send(:_from_rbseries, s._s.gather_with_series(key._s))
7027
+ end
7028
+
7029
+ def get_df_item_by_key(df, key)
7030
+ if key.size == 2
7031
+ row_key, col_key = key
7032
+
7033
+ # Support df[True, False] and df["a", "b"] as these are not ambiguous
7034
+ if Utils.bool?(row_key) || Utils.strlike?(row_key)
7035
+ return _select_columns(df, key)
7036
+ end
7037
+
7038
+ selection = _select_columns(df, col_key)
7039
+
7040
+ if selection.is_empty
7041
+ return selection
7042
+ elsif selection.is_a?(Series)
7043
+ return get_series_item_by_key(selection, row_key)
7044
+ else
7045
+ return _select_rows(selection, row_key)
7046
+ end
7047
+ end
7048
+
7049
+ key = key[0] if key.size == 1
7050
+
7051
+ # Single string input, e.g. df["a"]
7052
+ if Utils.strlike?(key)
7053
+ # This case is required because empty strings are otherwise treated
7054
+ # as an empty Sequence in `_select_rows`
7055
+ return df.get_column(key)
7056
+ end
7057
+
7058
+ # Single input - df[1] - or multiple inputs - df["a", "b", "c"]
7059
+ begin
7060
+ _select_rows(df, key)
7061
+ rescue TypeError
7062
+ _select_columns(df, key)
7063
+ end
7064
+ end
7065
+
7066
+ def _select_columns(df, key)
7067
+ if key.is_a?(Integer)
7068
+ return df.to_series(key)
7069
+
7070
+ elsif Utils.strlike?(key)
7071
+ return df.get_column(key)
7072
+
7073
+ elsif key.is_a?(Range)
7074
+ start, stop = key.begin, key.end
7075
+ if start.is_a?(::String)
7076
+ start = df.get_column_index(start)
7077
+ stop = df.get_column_index(stop)
7078
+ rng = Range.new(start, stop, key.exclude_end?)
7079
+ return _select_columns_by_index(df, rng)
7080
+ else
7081
+ return _select_columns_by_index(df, key)
7082
+ end
7083
+
7084
+ elsif key.is_a?(::Array)
7085
+ if key.empty?
7086
+ return df.class.new
7087
+ end
7088
+ first = key[0]
7089
+ if Utils.bool?(first)
7090
+ return _select_columns_by_mask(df, key)
7091
+ elsif first.is_a?(Integer)
7092
+ return _select_columns_by_index(df, key)
7093
+ elsif Utils.strlike?(first)
7094
+ return _select_columns_by_name(df, key)
7095
+ else
7096
+ msg = "cannot select columns using Sequence with elements of type #{first.class.name.inspect}"
7097
+ raise TypeError, msg
7098
+ end
7099
+
7100
+ elsif key.is_a?(Series)
7101
+ if key.is_empty
7102
+ return df.class.new
7103
+ end
7104
+ dtype = key.dtype
7105
+ if dtype == String
7106
+ return _select_columns_by_name(df, key)
7107
+ elsif dtype.integer?
7108
+ return _select_columns_by_index(df, key)
7109
+ elsif dtype == Boolean
7110
+ return _select_columns_by_mask(df, key)
7111
+ else
7112
+ msg = "cannot select columns using Series of type #{dtype}"
7113
+ raise TypeError, msg
7114
+ end
7115
+ end
7116
+
7117
+ msg = (
7118
+ "cannot select columns using key of type #{key.class.name.inspect}: #{key.inspect}"
7119
+ )
7120
+ raise TypeError, msg
7121
+ end
7122
+
7123
+ def _select_columns_by_index(df, key)
7124
+ series = key.map { |i| df.to_series(i) }
7125
+ df.class.new(series)
7126
+ end
7127
+
7128
+ def _select_columns_by_name(df, key)
7129
+ df.send(:_from_rbdf, df._df.select(Array(key)))
7130
+ end
7131
+
7132
+ def _select_columns_by_mask(df, key)
7133
+ if key.length != df.width
7134
+ msg = "expected #{df.width} values when selecting columns by boolean mask, got #{key.length}"
7135
+ raise ArgumentError, msg
7136
+ end
7137
+
7138
+ indices = key.each_with_index.filter_map { |val, i| i if val }
7139
+ _select_columns_by_index(df, indices)
7140
+ end
7141
+
7142
+ def _select_rows(df, key)
7143
+ if key.is_a?(Integer)
7144
+ num_rows = df.height
7145
+ if key >= num_rows || key < -num_rows
7146
+ msg = "index #{key} is out of bounds for DataFrame of height #{num_rows}"
7147
+ raise IndexError, msg
7148
+ end
7149
+ return df.slice(key, 1)
7150
+ end
7151
+
7152
+ if key.is_a?(Range)
7153
+ return _select_rows_by_slice(df, key)
7154
+
7155
+ elsif key.is_a?(::Array)
7156
+ if key.empty?
7157
+ return df.clear
7158
+ end
7159
+ if Utils.bool?(key[0])
7160
+ _raise_on_boolean_mask
7161
+ end
7162
+ s = Series.new("", key, dtype: Int64)
7163
+ indices = _convert_series_to_indices(s, df.height)
7164
+ return _select_rows_by_index(df, indices)
7165
+
7166
+ elsif key.is_a?(Series)
7167
+ indices = _convert_series_to_indices(key, df.height)
7168
+ return _select_rows_by_index(df, indices)
7169
+
7170
+ else
7171
+ msg = "cannot select rows using key of type #{key.class.name.inspect}: #{key.inspect}"
7172
+ raise TypeError, msg
7173
+ end
7174
+ end
7175
+
7176
+ def _select_rows_by_slice(df, key)
7177
+ Slice.new(df).apply(key)
7178
+ end
7179
+
7180
+ def _select_rows_by_index(df, key)
7181
+ df.send(:_from_rbdf, df._df.gather_with_series(key._s))
7182
+ end
7183
+
7184
+ def _convert_series_to_indices(s, size)
7185
+ idx_type = Plr.get_index_type
7186
+
7187
+ if s.dtype == idx_type
7188
+ return s
7189
+ end
7190
+
7191
+ if !s.dtype.integer?
7192
+ if s.dtype == Boolean
7193
+ _raise_on_boolean_mask
7194
+ else
7195
+ msg = "cannot treat Series of type #{s.dtype} as indices"
7196
+ raise TypeError, msg
7197
+ end
7198
+ end
7199
+
7200
+ if s.len == 0
7201
+ return Series.new(s.name, [], dtype: idx_type)
7202
+ end
7203
+
7204
+ if idx_type == UInt32
7205
+ if [Int64, UInt64].include?(s.dtype) && s.max >= Utils::U32_MAX
7206
+ msg = "index positions should be smaller than 2^32"
7207
+ raise ArgumentError, msg
7208
+ end
7209
+ if s.dtype == Int64 && s.min < -Utils::U32_MAX
7210
+ msg = "index positions should be greater than or equal to -2^32"
7211
+ raise ArgumentError, msg
7212
+ end
7213
+ end
7214
+
7215
+ if s.dtype.signed_integer?
7216
+ if s.min < 0
7217
+ if idx_type == UInt32
7218
+ idxs = [Int8, Int16].include?(s.dtype) ? s.cast(Int32) : s
7219
+ else
7220
+ idxs = [Int8, Int16, Int32].include?(s.dtype) ? s.cast(Int64) : s
7221
+ end
7222
+
7223
+ # Update negative indexes to absolute indexes.
7224
+ return (
7225
+ idxs.to_frame
7226
+ .select(
7227
+ F.when(F.col(idxs.name) < 0)
7228
+ .then(size + F.col(idxs.name))
7229
+ .otherwise(F.col(idxs.name))
7230
+ .cast(idx_type)
7231
+ )
7232
+ .to_series(0)
7233
+ )
7234
+ end
7235
+ end
7236
+
7237
+ s.cast(idx_type)
7238
+ end
7239
+
7240
+ def _raise_on_boolean_mask
7241
+ msg = (
7242
+ "selecting rows by passing a boolean mask to `[]` is not supported" +
7243
+ "\n\nHint: Use the `filter` method instead."
7244
+ )
7245
+ raise TypeError, msg
7246
+ end
5413
7247
  end
5414
7248
  end