85356


※12/28,「文字型」としていたが「データ型」に修正。ご指摘ありがとうございました。  
表題について、例えば列名とデータ型が以下の様なデータフレームがあり、この中から

  1. カラム名に"A"または"B"の文字列を含む列で、データ型がintのもののみ-1する
  2. カラム名に"0"を含むものは操作せず、何もしない

といった関数を実装するには、どのようなコードが必要でしょうか？

※今回は引数が2個なだけで、"A","B","Cの３つも引数に持てるような可変長にしたい

**対象のデータフレーム:**

[code]

    columns= ["A0","A1","A2","B0","B1","B2","C0","C1","C2","C3","X","Y","Z"]
    """
    "A0":str
    "B0":str
    "C0":str
    それ以外のカラム:int
    """
    
[/code]

イメージでいうと以下のような関数です。

[code]

    def make_new_df(not_minus1_column,*args):
        ~~~~
    make_new_df("0","A","B") 
    
[/code]

ですとか

[code]

    columns_list = ["A","B"]
       ~~~~
    make_new_df("0",columns_list)
    
[/code]

上記を実行すると、データフレームのカラム自体以下のままで、

[code]

    columns= ["A0","A1","A2","B0","B1","B2","C0","C1","C2","C3","X","Y","Z"]
    
[/code]

以下のカラム名の列の値だけが-1されるような関数を実装したいです。

[code]

    ["A1","A2","B1","B2"]
    # 他のCやXといった列は何も変わってない
    # 上記のカラムの値は元のデータフレームから全て-1されている
    
[/code]

重ねて、今回の引数はA,Bの2つですが、本当はA,B,Cの3つとか、A,B,C,Xの4つ等、可変長にしたいです。

元々の方針では、`def make_new_df(dont_minus1_column,minus1_column)` として

  1. 操作したくない列"dont_minus1_column(今回の例だと"0")"を除外する
  2. 引数をリストにして、containを用いてAを含むカラムだけの表を作成し、全体を-1する
  3. -1したらもともと除外した"0"を含むカラムにconcat(axis=1)でそのままくっつける
  4. 上記を一周として、for文でminus1_columnsの中にある["A","B"]をどんどんconcat(axis=1)して横に並べていく
  5. 選ばれなかったカラム名の列は、上記操作が終わってから再度付け足していく

という関数を実装したのですが、この場合例えばもし"0"が各文字の始点ではなく真ん中あたり(A1,A0,A2みたいな並び)だった場合に、元の並びとは異なってしまうため頓挫してしまいました。

分かりにくくて恐縮ですが、ぜひお教えいただきたく存じます。


CC BY-SA 4.0
2021-12-27T09:14:39.763
85354
2022-01-02T10:54:21.320
2021-12-28T05:52:17.463
48610
48610
question
python pandas
pandasのDataFrameの操作において、部分一致複数条件かつ可変長の引数を持って特定のカラムの値を操作する関数を実装したい


元の DataFrame書き換えてしまうけど, こんな風にできます

[code]

    import pandas as pd
    df = pd.DataFrame({"A0":['aa1','aa2'],"A1":[ 10, 15],"A2":[ 20, 25],
                       "B0":['bb1','bb2'],"B1":[110,115],"B2":[120,125],
                       "C0":['cc1','cc2'],"C1":[210,215],"C2":[220,225],"C3":[230,235],
                       "X":[300,305],"Y":[310,315],"Z":[320,325]})
    
    def make_new_df(nm, *cols):
        ndf = df[df.columns[
                ~df.columns.str.endswith(nm)
                & df.columns.str.startswith(tuple(cols))
        ]].sub(1)
        df.update(ndf)
        return ndf
    
    cols = ['A', 'B']
    ndf = make_new_df('0', *cols)
    display(ndf)
    df
    
[/code]

| A1 | A2 | B1 | B2  
---|---|---|---|---  
0 | 9 | 19 | 109 | 119  
1 | 14 | 24 | 114 | 124  
  
| A0 | A1 | A2 | B0 | B1 | B2 | C0 | C1 | C2 | C3 | X | Y | Z  
---|---|---|---|---|---|---|---|---|---|---|---|---|---  
0 | aa1 | 9 | 19 | bb1 | 109 | 119 | cc1 | 210 | 220 | 230 | 300 | 310 | 320  
1 | aa2 | 14 | 24 | bb2 | 114 | 124 | cc2 | 215 | 225 | 235 | 305 | 315 | 325  
  
* * *

#### 追記 (型チェック)

型が数値項目かどうかは, 後で付けれるだろうと 処理から省いてましたが, 何とかなりそうなので記しときます

[code]

    from pandas.api.types import is_numeric_dtype
    
    def make_new_df(nm, *cols):
        ndf = df[df.columns[
                ~df.columns.str.endswith(nm)
                & df.columns.str.startswith(cols)
                & [is_numeric_dtype(df[c]) for c in df.columns]
        ]].sub(1)
        df.update(ndf)
        return ndf
    
[/code]

* * *

#### 追記 (除外対象の指定方法)

上記の処理は, @kunif さんの回答の指摘にもあるように

  * 除外対象は, 文字列を含む, ではなく「～で終わる」という指定
  * 減算の対象リストには, 「～で始まる」という指定

除外の指定を `"BT"`と指定すると, その文字列が終端に付く項目を除外しようとします  
文字列を含む には次のような条件で可能です

[code]

        ndf = df[df.columns[
                ~df.columns.str.contains(nm)   # endswith => contains 変更
                & df.columns.str.startswith(cols)
        ]].select_dtypes(include='int').sub(1)
    
[/code]

(ついでに, 他の回答にあるように `select_dtypes` 使ってみました。どちらが効率よいか不明だけど, 特別に
importしなくてよいので便利かも)


CC BY-SA 4.0
2021-12-27T12:21:15.787
85355
2021-12-28T09:44:59.120
2021-12-28T09:44:59.120
43025
43025
85354
answer


> 考えられる要因は実際に用いたいデータフレームには欠損値が含まれていること

欠損値(`numpy.nan`)が含まれている場合、そのカラムのデータ型は `float` になります(`np.nan` のデータ型が `float`
だからです)。

[code]

    >>> import pandas as pd
    >>> import numpy as np
    >>> type(np.nan)
    <class 'float'>
    >>> columns = ['A0','A1','A2']
    >>> df = pd.DataFrame([[i]*len(columns) for i in range(2)], columns=columns)
    >>> df
       A0  A1  A2
    0   0   0   0
    1   1   1   1
    >>> df.dtypes
    A0    int64
    A1    int64
    A2    int64
    dtype: object
    >>> df.loc[1, 'A2'] = np.nan
    >>> df
       A0  A1   A2
    0   0   0  0.0
    1   1   1  NaN
    >>> df.dtypes
    A0      int64
    A1      int64
    A2    float64
    dtype: object
    
    # operation on `numpy.nan' is always `numpy.nan'
    >>> np.nan - 1
    np.nan
    
[/code]

なので、`float` 型も読み込む様にします。

> カラム名が"BT1"と"B1"のように一部重複している箇所があるためかと思われます。例えば、exclude_columns=[],
> update_columns=["B"]とすると、"BT"だけ-1され、"B"は-1されません(BTのほうがカラムとしては左側にあります)

考えれる原因は `B1`, `B2` カラムに `numpy.nan` が含まれていることです。`B1`, `B2` カラムの `dtype` が
`int` であれば問題はないからです。こちらは先の対応で直るかもしれません。

以下、変更したコードですが、`df.select_dtypes` に `float` 型を追加しただけです。

[code]

    import pandas as pd
    import numpy as np
    
    def make_new_df(df, exclude_columns=[], update_columns=[]):
      # columns which dtype is `int' or `float'(include `numpy.nan')
      columns = df.select_dtypes(include=['int', 'float']).columns
      # exclude
      if exclude_columns:
        columns = columns[~columns.str.contains('|'.join(exclude_columns))]
      # copy
      new_df = df.copy()
      # update
      if update_columns:
        columns = columns[columns.str.contains('|'.join(update_columns))]
        new_df[columns] -= 1
    
      return new_df
    
    if __name__ == '__main__':
      columns = ['A0','A1','A2','BT1','B0','B1','B2','C0','C1','C2','C3','X','Y','Z']
      df = pd.DataFrame([[i]*len(columns) for i in (0, 10)], columns=columns)
      df[['A0', 'B0', 'C0']] = ['a', 'b', 'c']
      df.loc[1, 'B2'] = np.nan
    
      #
      new_df = make_new_df(df, ['0'], ['A', 'B'])
    
      print(df.to_markdown(index=False))
      print(new_df.to_markdown(index=False))
    
[/code]

**df**  
| A0 | A1 | A2 | BT1 | B0 | B1 | B2 | C0 | C1 | C2 | C3 | X | Y | Z |  
|:----:|-----:|-----:|------:|:----:|-----:|-----:|:----:|-----:|-----:|-----:|----:|----:|----:|  
| a | 0 | 0 | 0 | b | 0 | 0 | c | 0 | 0 | 0 | 0 | 0 | 0 |  
| a | 10 | 10 | 10 | b | 10 | nan | c | 10 | 10 | 10 | 10 | 10 | 10 |

**new_df**  
| A0 | A1 | A2 | BT1 | B0 | B1 | B2 | C0 | C1 | C2 | C3 | X | Y | Z |  
|:----:|-----:|-----:|------:|:----:|-----:|-----:|:----:|-----:|-----:|-----:|----:|----:|----:|  
| a | -1 | -1 | -1 | b | -1 | -1 | c | 0 | 0 | 0 | 0 | 0 | 0 |  
| a | 9 | 9 | 9 | b | 9 | nan | c | 10 | 10 | 10 | 10 | 10 | 10 |

> 実データがない以上原因究明は難しいかもしれませんが

他にも特殊条件がありそうですが、今回は以上です。


CC BY-SA 4.0
2021-12-27T13:23:30.813
85356
2022-01-02T10:54:21.320
2022-01-02T10:54:21.320
47127
47127
85354
answer


@metropolis さん回答に示されたように、処理対象の列名リストが求められれば、単純に以下の1行で出来るでしょう。

`df`:処理対象DataFrame(結果を別オブジェクトにするなら@metropolis さん回答のようにコピーしておく)  
`columns`:処理対象列名リスト

[code]

    df[columns] -= 1
    
[/code]

* * *

処理対象の列名リストの求め方は、質問の①②の順番として以下で出来るでしょう。(①の-1処理は後で行います)  
`args`(必要なら`not_minus1_column`も)をリスト(orタプル)化する処理はあらかじめ行われているものとします。

ちなみに「カラム名に"A"または"B"の文字列を **含む** 列」とか「カラム名に"0"を **含む**
もの」とあったので`find()`を使いましたが、「～で始まる」とか「～で終わる」にしたい場合は @oriri
さん回答のように`startswith()`,`endswith()`を使えば良いでしょう。  
@metropolis さん回答のように正規表現を使う手もあるでしょう。

`not_minus1_column`:操作したくない列名に含まれる文字列(1個だけ)  
`args`:操作したい列名に含まれる文字列のリスト(複数でも可)

[code]

    columns = [n for n in df.select_dtypes(int).columns for c in args if n.find(c) >= 0]
    columns = list(dict.fromkeys(columns))
    columns = [n for n in columns if n.find(not_minus1_column) < 0]
    
[/code]

追記補足：操作したい列名文字列が重複すると同じ列名が多重にリストされていたのを修正(2行目を挿入)

訂正版：操作したくない列名に含まれる文字列の方もリストで行いたい場合は3行目が例えばこんな風になるでしょう。ただし思考実験としては書いてみましたが、こういう手順が増えるなら
@oriri さん、@metropolis さんの回答を採用した方が良さそうです。

[code]

    import re #### この行は先頭部の何処かに追記しておく
    
    exstr = '|'.join(not_minus1_column)
    columns = [n for n in columns if len(re.findall(exstr,n)) <= 0]
    
[/code]


CC BY-SA 4.0
2021-12-27T14:12:16.480
85357
2021-12-28T13:17:00.833
2021-12-28T13:17:00.833
26370
26370
85354
answer