@@ -861,8 +861,7 @@ def promote_offsets(self, col_id: str) -> OrderedIR:
861861 ## Methods that only work with ordering
862862 def project_window_op (
863863 self ,
864- column_name : ex .DerefOp ,
865- op : agg_ops .UnaryWindowOp ,
864+ expression : ex .Aggregation ,
866865 window_spec : WindowSpec ,
867866 output_name : str ,
868867 * ,
@@ -881,53 +880,66 @@ def project_window_op(
881880 # See: https://github.com/ibis-project/ibis/issues/9773
882881 used_exprs = map (
883882 self ._compile_expression ,
884- itertools .chain (
885- (column_name ,), map (ex .DerefOp , window_spec .all_referenced_columns )
883+ map (
884+ ex .DerefOp ,
885+ itertools .chain (
886+ expression .column_references , window_spec .all_referenced_columns
887+ ),
886888 ),
887889 )
888890 can_directly_window = not any (
889891 map (lambda x : is_literal (x ) or is_window (x ), used_exprs )
890892 )
891893 if not can_directly_window :
892894 return self ._reproject_to_table ().project_window_op (
893- column_name ,
894- op ,
895+ expression ,
895896 window_spec ,
896897 output_name ,
897898 never_skip_nulls = never_skip_nulls ,
898899 )
899900
900- column = typing .cast (ibis_types .Column , self ._compile_expression (column_name ))
901901 window = self ._ibis_window_from_spec (
902- window_spec , require_total_order = op .uses_total_row_ordering
902+ window_spec , require_total_order = expression . op .uses_total_row_ordering
903903 )
904904 bindings = {col : self ._get_ibis_column (col ) for col in self .column_ids }
905905
906906 window_op = agg_compiler .compile_analytic (
907- ex . UnaryAggregation ( op , column_name ) ,
907+ expression ,
908908 window ,
909909 bindings = bindings ,
910910 )
911911
912+ inputs = tuple (
913+ typing .cast (ibis_types .Column , self ._compile_expression (ex .DerefOp (column )))
914+ for column in expression .column_references
915+ )
912916 clauses = []
913- if op .skips_nulls and not never_skip_nulls :
914- clauses .append ((column .isnull (), ibis_types .null ()))
915- if window_spec .min_periods :
916- if op .skips_nulls :
917+ if expression .op .skips_nulls and not never_skip_nulls :
918+ for column in inputs :
919+ clauses .append ((column .isnull (), ibis_types .null ()))
920+ if window_spec .min_periods and len (inputs ) > 0 :
921+ if expression .op .skips_nulls :
917922 # Most operations do not count NULL values towards min_periods
923+ per_col_does_count = (column .notnull () for column in inputs )
924+ # All inputs must be non-null for observation to count
925+ is_observation = functools .reduce (
926+ lambda x , y : x & y , per_col_does_count
927+ ).cast (int )
918928 observation_count = agg_compiler .compile_analytic (
919- ex .UnaryAggregation (agg_ops .count_op , column_name ),
929+ ex .UnaryAggregation (agg_ops .sum_op , ex . deref ( "_observation_count" ) ),
920930 window ,
921- bindings = bindings ,
931+ bindings = { "_observation_count" : is_observation } ,
922932 )
923933 else :
924934 # Operations like count treat even NULLs as valid observations for the sake of min_periods
925935 # notnull is just used to convert null values to non-null (FALSE) values to be counted
926- denulled_value = typing . cast ( ibis_types . BooleanColumn , column . notnull () )
936+ is_observation = inputs [ 0 ]. notnull ()
927937 observation_count = agg_compiler .compile_analytic (
928- ex .UnaryAggregation (agg_ops .count_op , ex .deref ("_denulled" )),
938+ ex .UnaryAggregation (
939+ agg_ops .count_op , ex .deref ("_observation_count" )
940+ ),
929941 window ,
930- bindings = {** bindings , "_denulled " : denulled_value },
942+ bindings = {"_observation_count " : is_observation },
931943 )
932944 clauses .append (
933945 (
0 commit comments