Кросс-таблица с большим или undefined числом категорий
Моя реальная проблема связана с записью того, какое из очень большого количества антивирусных продуктов соглашается с тем, что данный образец является членом данного семейства антивирусов. База данных содержит миллионы выборок, при этом на каждый образец голосуют десятки антивирусных продуктов. Я хочу задать такой запрос, как "Для вредоносного ПО, содержащего имя" XYZ ", на выбор которого было больше всего голосов, и какие производители проголосовали за него?" и получить результаты вроде:
"BadBadVirus"
V1 V2 V3 V4 V5 V6 V7
Sample 1 - 4 votes 1 0 1 0 0 1 1
Sample 2 - 5 votes 1 0 1 0 1 1 1
Sample 3 - 5 votes 1 0 1 0 1 1 1
total 14 3 3 2 3 3
Что может быть использовано, чтобы сказать мне, что Vendor 2 и Vendor 4 либо не знают, как
для обнаружения этого вредоносного ПО, или что они называют его чем-то другим.
Я попытаюсь немного обобщить свой вопрос, надеясь, что не нарушу вашу способность помочь мне. Предположим, что у меня есть пять избирателей (Алекс, Боб, Кэрол, Дэйв, Эд), которых попросили посмотреть пять фотографий (P1, P2, P3, P4, P5) и решить, что такое "основной предмет" фотографии. Для нашего примера мы просто предположим, что они были ограничены "Cat", "Dog" или "Horse". Не каждый избиратель голосует за каждую вещь.
Данные находятся в базе данных в этой форме:
Photo, Voter, Decision
(1, 'Alex', 'Cat')
(1, 'Bob', 'Dog')
(1, 'Carol', 'Cat')
(1, 'Dave', 'Cat')
(1, 'Ed', 'Cat')
(2, 'Alex', 'Cat')
(2, 'Bob', 'Dog')
(2, 'Carol', 'Cat')
(2, 'Dave', 'Cat')
(2, 'Ed', 'Dog')
(3, 'Alex', 'Horse')
(3, 'Bob', 'Horse')
(3, 'Carol', 'Dog')
(3, 'Dave', 'Horse')
(3, 'Ed', 'Horse')
(4, 'Alex', 'Horse')
(4, 'Bob', 'Horse')
(4, 'Carol', 'Cat')
(4, 'Dave', 'Horse')
(4, 'Ed', 'Horse')
(5, 'Alex', 'Dog')
(5, 'Bob', 'Cat')
(5, 'Carol', 'Cat')
(5, 'Dave', 'Cat')
(5, 'Ed', 'Cat')
Цель состоит в том, что с учетом темы фото, которую мы ищем, мы хотели бы знать, сколько избирателей считало, что БЫЛО главное в этой фотографии, но также и список, КОТОРЫЕ ИЗБИРАТЕСЬ думали об этом.
Query for: "Cat"
Total Alex Bob Carol Dave Ed
1 - 4 1 0 1 1 1
2 - 3 1 0 1 1 0
3 - 0 0 0 0 0 0
4 - 1 0 0 1 0 0
5 - 4 0 1 1 1 1
------------------------------------
total 12 2 1 4 3 2
Query for: "Dog"
Total Alex Bob Carol Dave Ed
1 - 1 0 1 0 0 0
2 - 2 0 1 0 0 1
3 - 1 0 0 1 0 0
4 - 0 0 0 0 0 0
5 - 1 1 0 0 0 0
------------------------------------
total 5 1 2 1 0 1
Я что-то могу сделать с данными в том формате, который я сохранил?
Мне сложно получить запрос, который делает это - хотя это достаточно просто, чтобы выгрузить данные, а затем написать программу для этого, я бы очень хотел, чтобы это можно было сделать в БАЗЕ ДАННЫХ, если я могу.
Спасибо за любые предложения.
Ответы
Ответ 1
create table vote (Photo integer, Voter text, Decision text);
insert into vote values
(1, 'Alex', 'Cat'),
(1, 'Bob', 'Dog'),
(1, 'Carol', 'Cat'),
(1, 'Dave', 'Cat'),
(1, 'Ed', 'Cat'),
(2, 'Alex', 'Cat'),
(2, 'Bob', 'Dog'),
(2, 'Carol', 'Cat'),
(2, 'Dave', 'Cat'),
(2, 'Ed', 'Dog'),
(3, 'Alex', 'Horse'),
(3, 'Bob', 'Horse'),
(3, 'Carol', 'Dog'),
(3, 'Dave', 'Horse'),
(3, 'Ed', 'Horse'),
(4, 'Alex', 'Horse'),
(4, 'Bob', 'Horse'),
(4, 'Carol', 'Cat'),
(4, 'Dave', 'Horse'),
(4, 'Ed', 'Horse'),
(5, 'Alex', 'Dog'),
(5, 'Bob', 'Cat'),
(5, 'Carol', 'Cat'),
(5, 'Dave', 'Cat'),
(5, 'Ed', 'Cat')
;
Запрос для cat:
select photo,
alex + bob + carol + dave + ed as Total,
alex, bob, carol, dave, ed
from crosstab($$
select
photo, voter,
case decision when 'Cat' then 1 else 0 end
from vote
order by photo
$$,'
select distinct voter
from vote
order by voter
'
) as (
photo integer,
Alex integer,
Bob integer,
Carol integer,
Dave integer,
Ed integer
);
photo | total | alex | bob | carol | dave | ed
-------+-------+------+-----+-------+------+----
1 | 4 | 1 | 0 | 1 | 1 | 1
2 | 3 | 1 | 0 | 1 | 1 | 0
3 | 0 | 0 | 0 | 0 | 0 | 0
4 | 1 | 0 | 0 | 1 | 0 | 0
5 | 4 | 0 | 1 | 1 | 1 | 1
Если число избирателей велико или неизвестно, то это можно сделать динамически:
do $do$
declare
voter_list text;
r record;
begin
drop table if exists pivot;
voter_list := (
select string_agg(distinct voter, ' ' order by voter) from vote
);
execute(format('
create table pivot (
decision text,
photo integer,
Total integer,
%1$s
)', (replace(voter_list, ' ', ' integer, ') || ' integer')
));
for r in
select distinct decision from vote
loop
execute (format($f$
insert into pivot
select
%3$L as decision,
photo,
%1$s as Total,
%2$s
from crosstab($ct$
select
photo, voter,
case decision when %3$L then 1 else 0 end
from vote
order by photo
$ct$,$ct$
select distinct voter
from vote
order by voter
$ct$
) as (
photo integer,
%4$s
);$f$,
replace(voter_list, ' ', ' + '),
replace(voter_list, ' ', ', '),
r.decision,
replace(voter_list, ' ', ' integer, ') || ' integer'
));
end loop;
end; $do$;
Вышеприведенный код создал столбец таблицы со всеми решениями:
select * from pivot where decision = 'Cat';
Ответ 2
Ваше желание подразумевает передачу некоторых данных (имен)
в заголовки столбцов, то есть схему результирующей таблицы.
Поскольку это где-то между неудобным и невозможным,
Я бы рекомендовал просто сортировать и суммировать данные в sql,
и делать все остальное за пределами базы данных.
SELECT Photo, Voter
FROM data
WHERE Decision = '...'
ORDER BY Photo, Voter
и
SELECT Photo, COUNT(*) AS Total
FROM data
WHERE Decision = '...'
GROUP BY Photo
ORDER BY Photo;
Ответ 3
Используя те же данные примера, что и Clodoaldo ( "create table vote..." ) и используя функцию plpythonu make_pivot_table (ниже), вы можете запустить:
create temp table pivot_data on commit drop as
select * from vote where decision = 'Cat' union select photo, null, null from vote;
select * from make_pivot_table('{photo}', 'voter', 'decision', 'count', 'pivot_data',
'pivot_result', false);
select * from pivot_result order by photo;
Определение функции make_pivot_table:
-- make_pivot_table
-- python version 0.9
-- last edited 2015-08-11
create or replace function
make_pivot_table(row_headers text[], category_field text, value_field text,
value_action text, input_table text, output_table text, keep_result boolean)
returns void as
$$
# imports
from collections import defaultdict
import operator
import string
# constants
BATCH_SIZE = 100
VALID_ACTIONS = ('count', 'sum', 'min', 'max')
NULL_CATEGORY_NAME = 'NULL_CATEGORY'
TOTAL_COL = 'total'
# functions
def table_exists(tablename):
plan = plpy.prepare("""select table_schema, table_name from
information_schema.Tables where table_schema not in ('information_schema',
'pg_catalog') and table_name = $1""", ["text"])
rows = plpy.execute(plan, [input_table], 2)
return bool(rows)
def make_rowkey(row):
return tuple([row[header] for header in row_headers])
def quote_if_needed(value):
return plpy.quote_literal(value) if isinstance(value, basestring) else str(value)
# assumes None is never a value in the dct
def update_if(dct, key, new_value, op, result=True):
current_value = dct.get(key)
if current_value is None or op(value, current_value) == result:
dct[key] = new_value
def update_output_table(output_table, row_headers, colname, value):
pg_value = plpy.quote_literal(value) if isinstance(value, basestring) else value
sql = 'update %s set %s = %s where ' % (output_table, plpy.quote_ident(colname),
pg_value)
conditions = []
for index, row_header in enumerate(row_headers):
conditions.append('%s = %s' % (plpy.quote_ident(row_header),
quote_if_needed(rowkey[index])))
sql += ' and '.join(conditions)
plpy.execute(sql)
# -----------------
if not table_exists(input_table):
plpy.error('input_table %s dones not exist' % input_table)
if value_action not in VALID_ACTIONS:
plpy.error('%s is not a recognised action' % value_action)
# load the data into a dict
count_dict = defaultdict(int)
sum_dict = defaultdict(float)
total_dict = defaultdict(float)
min_dict = dict()
max_dict = dict()
categories_seen = set()
rowkeys_seen = set()
do_total = value_action in ('count', 'sum')
cursor = plpy.cursor('select * from %s' % plpy.quote_ident(input_table))
while True:
rows = cursor.fetch(BATCH_SIZE)
if not rows:
break
for row in rows:
rowkey = make_rowkey(row)
rowkeys_seen.add(rowkey)
category = row[category_field]
value = row[value_field]
dctkey = (rowkey, category)
# skip if value field is null
if value is None:
continue
categories_seen.add(category)
if value_action == 'count':
count_dict[dctkey] += 1
total_dict[rowkey] += 1
if value_action == 'sum':
sum_dict[dctkey] += value
total_dict[rowkey] += value
if value_action == 'min':
update_if(min_dict, dctkey, value, operator.lt)
if value_action == 'max':
update_if(max_dict, dctkey, value, operator.gt)
plpy.notice('seen %s summary rows and %s categories' % (len(rowkeys_seen),
len(categories_seen)))
# get the columns types
coltype_dict = dict()
input_type_sql = 'select * from %s where false' % plpy.quote_ident(input_table)
input_type_result = plpy.execute(input_type_sql)
for index, colname in enumerate(input_type_result.colnames()):
coltype_num = input_type_result.coltypes()[index]
coltype_sql = 'select typname from pg_type where oid = %s' % coltype_num
coltype = list(plpy.cursor(coltype_sql))[0]
plpy.notice('%s: %s' % (colname, coltype['typname']))
coltype_dict[colname] = coltype['typname']
plpy.execute('drop table if exists %s' % plpy.quote_ident(output_table))
sql_parts = []
if keep_result:
sql_parts.append('create table %s (' % plpy.quote_ident(output_table))
else:
sql_parts.append('create temp table %s (' % plpy.quote_ident(output_table))
cols = []
for row_header in row_headers:
cols.append('%s %s' % (plpy.quote_ident(row_header), coltype_dict[row_header]))
cat_type = 'bigint' if value_action == 'count' else coltype_dict[value_field]
for col in sorted(categories_seen):
if col is None:
cols.append('%s %s' % (plpy.quote_ident(NULL_CATEGORY_NAME), cat_type))
else:
cols.append('%s %s' % (plpy.quote_ident(col), cat_type))
if do_total:
cols.append('%s %s' % (TOTAL_COL, cat_type))
sql_parts.append(',\n'.join(cols))
if keep_result:
sql_parts.append(')')
else:
sql_parts.append(') on commit drop')
plpy.execute('\n'.join(sql_parts))
dict_map = {'count': count_dict, 'sum': sum_dict, 'min': min_dict, 'max': max_dict }
value_dict = dict_map[value_action]
for rowkey in rowkeys_seen:
sql = 'insert into %s values (' % plpy.quote_ident(output_table)
sql += ', '.join([quote_if_needed(part) for part in rowkey])
sql += ')'
plpy.execute(sql)
if do_total:
for rowkey, value in total_dict.iteritems():
update_output_table(output_table, row_headers, TOTAL_COL, value)
for (rowkey, category), value in value_dict.iteritems():
# put in cateogry value
colname = NULL_CATEGORY_NAME if category is None else category
update_output_table(output_table, row_headers, colname, value)
$$ language plpythonu