質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

pandas

Pandasは、PythonでRにおけるデータフレームに似た型を持たせることができるライブラリです。 行列計算の負担が大幅に軽減されるため、Rで行っていた集計作業をPythonでも比較的簡単に行えます。 データ構造を変更したりデータ分析したりするときにも便利です。

Q&A

解決済

2回答

2450閲覧

部分重複している2行の内、ある要素が小さい方の行を削除したい

yoppi2019

総合スコア8

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

pandas

Pandasは、PythonでRにおけるデータフレームに似た型を持たせることができるライブラリです。 行列計算の負担が大幅に軽減されるため、Rで行っていた集計作業をPythonでも比較的簡単に行えます。 データ構造を変更したりデータ分析したりするときにも便利です。

0グッド

0クリップ

投稿2019/12/27 05:01

前提・実現したいこと

Python初学者でPython3.8、Pandas0.25.3を使用しています。

ある試験の結果表(test_result.csv)から、不要な行を削除したものを成績表として転用しています。

  • 試験は毎年、A(受験必須)とB(受験任意)の2種類が実施
  • 成績は、Aのみの受験者はAの点数、A/B両方の受験者は高かった方の点数

つまり、Aしか受験していない人については、試験の結果=成績としてそのまま使えますが、
A/B両方を受験した人は、同年度に試験結果が2行あるため、低得点の行を削除する必要があります。

例えば17年度ですと、小島さんだけがA/B両方を受験しているため、成績表はこうなります。
|氏名|年度|点数|種類|
|:--|:--:|--:|
田中|17|82|A|
松本|17|64|A|
吉田|17|58|A|
鈴木|17|51|A|
小島|17|92|B|

この「低得点の行を削除(または高得点の行を抽出)」というところでつまづいています。


test_result.csv の内容

氏名,年度,点数,種類
田中,17,82,A
松本,17,64,A
吉田,17,58,A
鈴木,17,51,A
小島,17,88,A
小島,17,92,B
田中,18,80,A
田中,18,78,B
松本,18,66,A
吉田,18,63,A
鈴木,18,45,A
小島,18,82,A
小島,18,79,B
田中,19,78,A
田中,19,85,B
松本,19,68,A
松本,19,62,B
吉田,19,68,A
鈴木,19,48,A
小島,19,82,A
小島,19,94,B


該当のソースコード

python

1import pandas as pd 2 3df = pd.read_csv("test_result.csv",encoding="shift_jis", index_col=None) 4 5#できれば氏名・年度リストを使わずに済む方法を調べる 6p = ["田中","松本","吉田","鈴木","小島","石本"] 7y = [17,18,19] 8 9#氏名ごとに行抽出 10for person in p: 11 df_A = df[df["氏名"] == person] 12 #そこから年度ごとに行抽出することで、部分重複行を特定 13 for year in y: 14 df_B = df_A[df_A["年度"] == year] 15 #部分重複行数がなければそのまま成績表に使えるのでパス 16 if len(df_B) <= 1: 17 pass 18 else: 19 #点数が小さい方のインデックスを取得して、当インデックスの行を削除 20 print([df_B["点数"].idxmin()])#うまく行かなかったのでprintで確認 21 22 for i in [df_B["点数"].idxmin()]: 23 df_new = df.drop(index=i) 24 df_new.to_csv("aaa.csv",encoding="shift_jis") 25 26#[7] 27#[13] 28#[16] 29#[4] 30#[12] 31#[19] 32#結果、aaa.csvではindex=19の行のみ削除 33

以上を踏まえ、ご質問が2点あります。
初学者につき、理解に不足や誤認があると思いますので、ご指摘宜しくお願いします。

まず、吐き出されたcsvでindex=19の行しか削除されていないのは、削除したい行のindexが
[7][13]というように別個のリストに格納されているため、最後に部分重複行としてヒットした
index=19だけにdropが適用された、ということかなあと思っています。

そしてその原因は、部分重複行を抽出するプロセスでfor person in p:というfor文を使ったため、
と思っています。

また、indexのリストを[7,13,16,・・・]と1つのリストに格納できれば、dropすれば成績表として
吐き出すことができるのではないかと思っています。

以上の結果、
for文を使わずに部分重複行を抽出する方法が思い浮かばない ⇒
for文を使った上で、削除したいindexを1リストに格納する方法が分からない ⇒ 詰み
となってしまったところです。

そこで「for文で部分重複行を抽出した上で、削除したいindexを1リストに格納する」という
方法自体は可能なのでしょうか。可能であればどう修正すれば良いでしょうか。
また、可能だけどもっと良い方法があるよというアドバイスを頂けないでしょうか。

二点目に、上記コードでは部分重複行を抽出するため、予め氏名・年度をn,yでリスト化しています。

これは部分重複行の抽出方法として他に思い浮かばなかったための苦肉の策で(というのも、
実際の受験者は約70名、内、約1割が毎年入れ替わります)、あまり効率的ではありません。

そこでリストを使わず部分重複行を抽出する方法があれば、併せてアドバイス頂けると嬉しいです。

以上、長文お読み頂きありがとうございました。宜しくお願い致します。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

このように for文で処理を行いたいのであれば、for文の前に

df_new = df.copy()

のように予め dfのコピーを作っておき、削除の部分は

Python

1df_new = df_new.drop(index=i)

のようにコピーしたdfから順々に削除を行うとよいのではないでしょうか。

補足として、for文の書き方ですが、

Python

1p = ["田中","松本","吉田","鈴木","小島","石本"] 2for person in p: 3 df_A = df[df["氏名"] == person]

の部分は

for person, df_A in df.groupby('氏名'):

と1行にまとまりますし、内部の

Python

1y = [17,18,19] 2for year in y: 3 df_B = df_A[df_A["年度"] == year]

の部分も同様に

Python

1for year in df_A.groupby("年度"):

と書けます。

更には、上の2つのループを纏めて

for (person, year), df_B, in df.groupby(['氏名', '年度']):

となりますので、この DataFrame.groupby()は是非覚えてください。

以下まとめると

Python

1import pandas as pd 2 3df = pd.read_csv("test_result.csv",encoding="shift_jis", index_col=None) 4 5df_new = df.copy() 6for (person, year), df_B, in df.groupby(['氏名', '年度']): 7 #部分重複行数がなければそのまま成績表に使えるのでパス 8 if len(df_B) <= 1: 9 continue 10 11 df_new = df_new.drop(index=df_B["点数"].idxmin()) 12df_new.to_csv("aaa.csv",encoding="shift_jis")

でよいのではないでしょうか。

for文を使わずに部分重複行を抽出する方法が思い浮かばない

上の DataFrame.groupby()が解ると groupby.apply()を使うと上記の処理が1行で書けます。

ret = df.groupby(['氏名', '年度']).apply(lambda d: d.loc[d['点数'].idxmax()]).reset_index(drop=True) # 氏名 年度 点数 種類 #0 田中 17 82 A #1 松本 17 64 A #2 吉田 17 58 A #3 鈴木 17 51 A #4 小島 17 92 B #5 田中 18 80 A #6 松本 18 66 A #7 吉田 18 63 A #8 鈴木 18 45 A #9 小島 18 82 A #10 田中 19 85 B #11 松本 19 68 A #12 吉田 19 68 A #13 鈴木 19 48 A #14 小島 19 94 B

投稿2019/12/27 05:57

編集2019/12/27 05:59
magichan

総合スコア15898

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

magichan

2019/12/27 06:03

既にhayataka2049さんも groupbyを使った回答を投稿されてますね。 記述は少し違いますが(groupbyで必要な Indexを抽出している)、考え方は同じです。
magichan

2019/12/27 06:06

あと、ソースコードに できれば氏名・年度リストを使わずに済む方法を調べる とありますが、その場合は p = df['氏名'].unique() のように unique() を使います。
yoppi2019

2019/12/27 06:52

すごく丁寧に答えて頂いて、ありがとうございます。ざっと目を通した限り、つい今しがた調べたgroupbyの他にも、知らない内容が沢山あり、理解するのに時間がかかりそうなので、取り急ぎお礼申し上げます。改めて、お返事させて頂きます!
yoppi2019

2019/12/27 07:52

何かしら解決方法はあるはずと思い質問させて頂いたのですが、結果1行でできるとは思ってもいませんでした。ただ、この一行コードはちょっと現状の私の理解レベルとの差が大きいようですので、また暫く勉強した後に改めて理解チャレンジさせて頂きます。 内包表記は、ネットで調べている時に見たことはありましたが、初めて見たのがforの使い方も分からなかった頃でしたので、ちょっと何言ってるのか分かんないという意識が残ったせいか、避けていました。が、これを機に積極的に使うべく、一度じっくり勉強してみます。
yoppi2019

2019/12/27 08:03

いまいち理解できていないのがコピーする意味なのですが、これは私がdf_newを定義したため、それに合わせて強いてやるならコピー、ということなのでしょうか。(コピーを作らず、コードのdf_newを全てdfに置換してみたら、同じ結果になりましたので)
yoppi2019

2019/12/27 08:03

また、とてもシンプルな問題解決方法をご提示下さったhayataka2049さん、初学者の要望にお付き合い頂き、+αの知識まで非常に丁寧に教えて下さったmagichanさんのご両名にベストアンサーを差し上げたいのですが、どちらも私の抱えていた問題を余すことなく解決頂けるような内容でしたので、先にお答え頂いたhayataka2049さんにBAを差し上げたく思います。せっかく素晴らしい回答を頂いたのに、申し訳ありません><
guest

0

ベストアンサー

そこまで凝らなくてもgroupbyでできるだろうというだけの回答です。

python

1import io 2import pandas as pd 3 4csv_data = """ 5氏名,年度,点数,種類 6田中,17,82,A 7松本,17,64,A 8吉田,17,58,A 9鈴木,17,51,A 10小島,17,88,A 11小島,17,92,B 12田中,18,80,A 13田中,18,78,B 14松本,18,66,A 15吉田,18,63,A 16鈴木,18,45,A 17小島,18,82,A 18小島,18,79,B 19田中,19,78,A 20田中,19,85,B 21松本,19,68,A 22松本,19,62,B 23吉田,19,68,A 24鈴木,19,48,A 25小島,19,82,A 26小島,19,94,B 27""" 28 29df = pd.read_csv(io.StringIO(csv_data)) 30idx = df.groupby(["年度", "氏名"])["点数"].idxmax() 31result = df.iloc[idx].sort_index() 32print(result) 33""" => 34 氏名 年度 点数 種類 350 田中 17 82 A 361 松本 17 64 A 372 吉田 17 58 A 383 鈴木 17 51 A 395 小島 17 92 B 406 田中 18 80 A 418 松本 18 66 A 429 吉田 18 63 A 4310 鈴木 18 45 A 4411 小島 18 82 A 4514 田中 19 85 B 4615 松本 19 68 A 4717 吉田 19 68 A 4818 鈴木 19 48 A 4920 小島 19 94 B 50""" 51

投稿2019/12/27 05:22

hayataka2049

総合スコア30933

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yoppi2019

2019/12/27 06:45

ここ数日間悩んだ時間は何だったのか・・・という思いです。 課題だと思っていたものが全て解決できました。 ioもgroupbyも初めて見たもので、ちょっとお返事する前に調べようと思っていたら時間がかかってしまいました。
hayataka2049

2019/12/27 06:46

わかると思いますが、ioはどうでも良いです。csvファイルを別に用意するよりコードに埋め込みたかっただけで。実際はcsvファイルでやっていただいて結構です。
yoppi2019

2019/12/27 06:46

まだうっすらとしか理解できていないのですが、氏名と年度をグループ化したものをキーのようにして部分重複行を抽出していると理解しましたが、ちょっとそんな発想は全くありませんでした・・・。確かに、そうであれば、とてもシンプルに重複しているかどうかチェックできますよね。
yoppi2019

2019/12/27 06:48

やはり自分が何回か使ったことのある内容だけで解決しようとすると思考が狭まって、見る人から見るとややこしいことやっているなーと思えるものなのですね。 作業する前にもっと色々な可能性を考えるようにしたいと思います。大変勉強になりました!ありがとうございました。
yoppi2019

2019/12/27 06:49

>わかると思いますが、ioはどうでも良いです お恥ずかしながら、分からなかったので併せて調べました・・・。今回のように長いcsvデータがあるような質問に回答されるときに便利なモジュールなんだろうな、というくらいの感じに理解しました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問